React - setting up contextProvider that uses a remote API - avoiding multiple calls during re rendering

39 Views Asked by At

An application I am working on stores some core remote information that it only really needs to load once. I am aware I can cache the remote call via localstorage and retrieve it from there, but for the purposes of this question lets assume I just want to avoid calling the fetchState function multiple times during rendering and rerendering

Below is the root app.

My question is I assume I am handling the context incorrectly. Is there a better way to handle this. The multiple state updates during the api call can be combined into a reducer (I assume). Leading to only 1 re render.

However I noted that it basically loses state when I navigate to a new page via the route. IE when I navigate to page 1 the App state appears to reset to null. This causes a context update as well. I assume that the routing destroys and rerenders the entire app? is that correct?


import {getUser} from "./authenticator/CredentialManager";

export const UserContext = createContext(
    {user: getUser(),
        setUser:()=>{}
    });
export const LocationContext = createContext(null)
function App() {
    const [user,setUser] = useState(getUser());
    const [location, setLocation] = useState(null);
    const [schedule,setSchedule] = useState(null);
    const update =
        useMemo(
            () => (
                {user,setUser}
            ),[user]);
    const fetchState = ()=>{
        const api = new VetbookWpApi();
        if(location){
            if(location.id){
                return;
            }
        }
        api.getLocation().then((result) => {
            let output = JSON.parse(result);
            let location = null
            if (Array.isArray(output)) {
                location = output[0];
            } else
                location = output;
            setLocation(location); // <--triggers rerender
            return location;
        },(e)=>{
            console.log(e);
        }).then((response) => {
            if (response) {
                api.getSchedules(response.id).then((result) => {
                    setSchedule(JSON.parse(result)); // <--triggers rerender
                });
            }
        });
    }
    useEffect(() => {
           fetchState();
           },
        []
    );

    return (
        <div className="App">
            <LocationContext.Provider value={location}>
            <UserContext.Provider value={update}>
                <Navigation/>
                <Main/>
            </UserContext.Provider>
            </LocationContext.Provider>
        </div>
    );
}

const Main = () => {
    return (
        <div className="App">
            <Routes>
                <Route path='/' element={<Home/>}></Route>
                <Route path='/page1' element={<Page1/>}></Route>
                <Route path='/page2' element={<Page2/>}></Route>
                <Route path='/page3' element={<Page3/>}></Route>
            </Routes>
        </div>
    )
}

const Navigation = () => {
    const location = useContext(LocationContext);
    const [anchorEl, setAnchorEl] = useState(null);
    const open = Boolean(anchorEl);
    const handleClick = (event) => {
        setAnchorEl(event.currentTarget);
    };
    const handleClose = () => {
        setAnchorEl(null);

    };
    let name = '';
    if(location){
        name = location.name;
    }
    return (
        <AppBar position='static'>
            <Container maxWidth='1'>
                <Toolbar disableGutters>
                    <IconButton
                        id="fade-button"
                        aria-controls={open ? 'fade-menu' : undefined}
                        aria-haspopup="true"
                        aria-expanded={open ? 'true' : undefined}
                        onClick={handleClick}
                        color='inherit'
                        sx={{ mr: 2 }}
                    ><MenuIcon/></IconButton>
                    <Menu
                        id="fade-menu"
                        MenuListProps={{
                            'aria-labelledby': 'fade-button',
                        }}
                        anchorEl={anchorEl}
                        open={open}
                        onClose={handleClose}
                        TransitionComponent={Fade}
                    >
                        <MenuItem component='a' href='/' onClick={handleClose}>Home</MenuItem>
                        <MenuItem component='a' href='/Page1' onClick={handleClose}>Page1</MenuItem>
                        <MenuItem component='a' href='/Page2' onClick={handleClose}>Page2</MenuItem>
                    </Menu>
                    <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>VetBook - {name}</Typography>
                    <LoginButton/>
                </Toolbar>
            </Container>
        </AppBar>
    )
};
1

There are 1 best solutions below

1
danielRICADO On BEST ANSWER

I'll not get into deconstructing the example, but the Gist is having the state provider expose a function that updates itself rather than the App Component pushing updates to it via props.

Here's a clean starter for you to work from.

create a "StateProvider"

import React, {createContext, useContext, useState} from 'react';

const StateContext = createContext<any | undefined>(undefined);

export function useStateContext() {
  return useContext(StateContext);
}

export function StateProvider({children}) {
  const [appState, setAppState] = useState({
    user: null,
  });

  const updateState = newState => {
    setAppState(newState);
  };

  const providerValue = {appState, updateState};

  return (
    <StateContext.Provider value={providerValue}>
      {children}
    </StateContext.Provider>
  );
}

wrap up the app with the provider

export default function App() {
  return (
    <StateProvider>
      <AppLayout />
    </StateProvider>
  );
}

now in any component we can get and set the application state

import {useStateContext} from '../state';

...

const {appState, updateState} = useStateContext();

const user = // fetch the user from the api

updateState(prevState => ({
  ...prevState,
  user: user,
}));

The state should persist across routes, if you need to to retain for hard browser refreshes then have the State provider write it to localstorage and check for and restore that data when the app loads again