Before you start, makesure that you follow the backend setup guide to have the server successfully running on your machine
Also, we need use axios (a Promise based HTTP client for the browser and node.js) to make API calls to the server. This JS package is newly added, so let's install it on your machine.
- make sure you are in the
frontend
folder - run command
yarn install
- you should see axios being added
Having the setup done, let's start both the frontend and the server:
- start frontend: run
yarn start
in thefrontend
folder as we usually do - start server (you may open a new terminal to do this):
- before you start, make sure you are in the python virtual environment (you should see
(env)
in your terminal)you can activate the virtual environment by command:
This time you should be in the root project folder
On Windows:\env\Scripts\activate.bat
On MacOS:source env/bin/activate
- start the server by running
python manage.py runserver
- you should see ther server running at
http://localhost:8000
- before you start, make sure you are in the python virtual environment (you should see
We will be using fetch games
, which is a GET request as an example
-
Add API functions that will be used to call the backend APIs. API functions should be put in
frontend/src/services/api
Note that we try to categorize the API functions for better maintainability. API functions should be put in the corresponding folder.
E.g., if this api is game related, it should be put infrontend/src/services/api/game/index.js
Create a new folder (e.g.,/dashboard
) under the/api
folder and add anindex.js
file in that folder if they do not already exist.Sample code of fetch games in
frontend/src/services/api/game/index.js
:// add this import statement before you write api functions if it is not added import yelp from '../../yelp'; // api function to call 'localhost:8000/hufgames' export const fetchGames = async () => { try { const response = await yelp.get('/hufgames', {}); return response; } catch (err) { return err.message; } };
-
Add states and functions that updates the state in the corresponding data store. Data stores are located in
frontend/src/services/zustand
A data store looks like this:
import create from 'zustand'; // import statements for api functions // other import statements, if any const initialState = { // initial states with default value // (normally set to null, an empty string '', or an empty array []) }; // the data store hook that consists of states & functions that update states // by convention, the hook is named as `use{what this store is about}Store` // e.g., if this store is for game data, then the hook is named `useGameStore` export const useXXXStore = create((set, get) => ({ ...initialState, // initialize the initialState (i.e., default states) // functions definitions }));
For example, this is how our game store looks like:
import create from 'zustand'; // Step 1: import the API function for fetching games in the /api/game folder import { fetchGames } from '../../api/game'; // Step 2: add initial states const initialState = { isLoading: false, // state to indicate the data is loading games: [], // at the begining, we have an empty array of game objects }; // game store hook export const useGameStore = create((set, get) => ({ // activate the initial states (you don't need to do anything here) ...initialState, // Step 3: add a function to update the state we need to use (e.g., games and isLoading). // This function will be called in our page components (e.g., GamesPage) // It should be async by default. fetchGames: async () => { // set the isLoading to true, so our UI can show loading animation depending on the value of isLoading state set({ isLoading: true }); // call the API function, which returns the response from the server const result = await fetchGames(); // if a string is returned, it means it is an error message if (typeof result === 'string') { // return the error message directly return result; } else { // if the result is not a string, it means the API call is successful // set the data in the response result to the `games` state (i.e., update the `games` state) const games = result.data; set({ games: games }); } // state is updated, set loading state to false set({ isLoading: false }); }, }));
You can simply follow the store sturcture above (copy & paste) to create a new data store if it is not created yet.
-
Now we have our data store, we are ready to update our page UI using the states and functions from the data store.
-
Open the page that needs data from the backend. (
GamesPage
in this case, which is located atfrontend/src/pages/GamesPage/index.js
). -
Import the datastore by adding a line:
import { useGameStore } from '../../services/zustand/game';
-
Get the state and function we need from the data store in our page component:
const GamesPage = () => { // states: isLoading, games // function: fetchGames const { isLoading, games, fetchGames } = useGameStore(); ... }
-
Call the function to fetch data wherever needed and supply the corresponding UI components with the data in the state:
isLoading
:<div className='games-container'> ... <List <!-- pass the isLoading state to the skeleton componnet, so that it can show the loading animation accordingly --> loading={isLoading} dataSource={filteredGames} renderItem={(item, index) => ( <List.Item key={JSON.stringify(item) + index}> <!-- pass the isLoading state to the skeleton componnet, so that it can show the loading animation accordingly --> <Skeleton loading={isLoading} active> <GameCard title={item.game_name} creator={item.user_id} tag={item.game_tag} onClick={() => handleOnClickGameCard(item)} /> </Skeleton> </List.Item> )} /> </div>
games
&fetchGames
:const [filteredGames, setFilteredGames] = React.useState([]); // useEffect hook to call the fetchGames function when the page is rendered React.useEffect(() => { const fetchData = async () => { // remember? the fetchGames will return an error message when the api call is not successful const errorMessage = await fetchGames(); // if this error message is not null if (errorMessage) { // prompt the user error messages message.error('Failed to fetch games. Contact Admin for support.'); message.error(errorMessage); } else { // prompt the user messages (optional since it is successful) message.success('Successfully fetched latest games list'); } }; fetchData(); }, [fetchGames]); React.useEffect(() => { // set the initial state of filteredGames to the games fetched from the server setFilteredGames(games); }, [games]); // function to handle the search operation const handleOnSearch = React.useCallback( (searchValue) => { const lowercaseSearchValue = searchValue.toLowerCase(); // filter games based on search string const updatedFilteredGames = games.filter((game) => game.game_name.toLowerCase().includes(lowercaseSearchValue) ); setFilteredGames(updatedFilteredGames); }, [games] ); <List loading={isLoading} <!-- pass the filteredGames (which is initial as games) to the list componnet, so that it can render the game cards accordingly --> dataSource={filteredGames} renderItem={(item, index) => ( <List.Item key={JSON.stringify(item) + index}> <Skeleton loading={isLoading} active> <GameCard title={item.game_name} creator={item.user_id} tag={item.game_tag} onClick={() => handleOnClickGameCard(item)} /> </Skeleton> </List.Item> )} />
-