How to set an `imagesLoaded` state within useAppIsReady in App.tsx - reactjs

I have a react native app. I have a hook useAppIsReady that contains a check for whether images have been loaded yet located in my App.tsx.
The way it works is I have state in my context numberOfImagesLoaded. I have multiple images loaded within Home. When an image loads, numberOfImagesLoaded gets incremented by 1, and once it reaches a specified number, appIsReady gets set to true.
The problem is that I can't use useAppIsReady within my App.tsx since numberOfImagesLoaded is located within my context, and The return statement that contains my whole app wrapped in context is defined after useAppIsReady gets called, so I'm forced to place useAppIsReady inside my Home component instead, which is suboptimal. How can I make useAppIsReady work in App.tsx?
I don't want to define the numberOfImagesLoaded state in my App.tsx because it will be a pain to pass down through props.
useAppIsReady
const useAppIsReady = () => {
const [appIsReady, setAppIsReady] = useState(false);
const context = useContext(AppContext);
// If all images are loaded
const imagesAreLoaded = context.numberOfImagesLoaded > 5;
if (imagesAreLoaded && !appIsReady) {
setAppIsReady(true);
}
return appIsReady;
};
App.tsx
SplashScreen.preventAutoHideAsync();
export default function App() {
// useAppIsReady() won't work since context has not yet been defined
const appIsReady = useAppIsReady();
const Tab = createBottomTabNavigator<AppParamList>();
useEffect(() => {
const prepare = async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
}
};
prepare();
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<AppProvider>
<NavigationContainer>
<Tab.Navigator
>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen
name="MyQueues"
component={MyQueuesScreen}
/>
<Tab.Screen name="Profile" component={Profile} />
</Tab.Navigator>
</NavigationContainer>
</AppProvider>
);
}

You can either lift the appIsReady state up (e.g. into <AppProvider>) where you have access to the numberOfImagesLoaded state and use it there.
Finally, you pass appIsReady to a child component or provide it via context, where you use and handle the ready state.
Here's an example:
const AppProvider = () => {
...
const [numberOfImagesLoaded, setNumberOfImagesLoaded] = useState(0);
const appIsReady = useAppIsReady(numberOfImagesLoaded);
return (
<YourContext.Provider value={{numberOfImagesLoaded, appIsReady}}>
{children}
</YourContext.Provider>
);
}
const useAppIsReady = ({numberOfImagesLoaded}) => {
...
useEffect(() => {
// your ready logic
},[numberOfImagesLoaded]);
return appIsReady;
}
App or whichever needed component can use the context and access appIsReady.

Related

How to work around needing to access context state within the entry point of my App before the Provider has been defined

I have a react native app. I have a hook useAppIsReady that contains a check for whether images have been loaded yet located in my App.tsx.
The way it works is I have state in my context numberOfImagesLoaded. I have multiple images loaded within Home. When an image loads, numberOfImagesLoaded gets incremented by 1, and once it reaches a specified number, appIsReady gets set to true.
The problem is that I can't use useAppIsReady within my App.tsx since numberOfImagesLoaded is located within my context, and useAppIsReady is used within App.tsx, the entry point to my application, so the return statement that contains my whole app wrapped in context is defined after useAppIsReady gets called. So I'm forced to place useAppIsReady inside a child component Home instead, which is suboptimal. How can I make useAppIsReady work in my app's entry point, App.tsx?
(btw I don't want to define the numberOfImagesLoaded state in my App.tsx because it will be a pain to pass down through props)
useAppIsReady
const useAppIsReady = () => {
const [appIsReady, setAppIsReady] = useState(false);
const context = useContext(AppContext);
// If all images are loaded
const imagesAreLoaded = context.numberOfImagesLoaded > 5;
if (imagesAreLoaded && !appIsReady) {
setAppIsReady(true);
}
return appIsReady;
};
App.tsx
SplashScreen.preventAutoHideAsync();
export default function App() {
// useAppIsReady() won't work since context has not yet been defined
const appIsReady = useAppIsReady();
const Tab = createBottomTabNavigator<AppParamList>();
useEffect(() => {
const prepare = async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
}
};
prepare();
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<AppProvider>
<NavigationContainer>
<Tab.Navigator
>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen
name="MyQueues"
component={MyQueuesScreen}
/>
<Tab.Screen name="Profile" component={Profile} />
</Tab.Navigator>
</NavigationContainer>
</AppProvider>
);
}

React (+ Typescript) component not rerendering upon updating Context

I have a LaunchItem component which uses React.Context to get and set information to/from the local storage.
What I am trying to achieve is that, when the component updates the Context (and local storage), I want it to rerender with the new information, so that it then updates the state of a local button.
The problem is, although the Context seems to be updated as well as the contents of the local storage, the item is not rerendered. (when I refresh the page I can see the button has changed state, however, signifying that it is able to derive that information from the Context just fine.
I will now share some code and hopefully someone is able to understand what I might be missing, I thoroughly appreciate your help :)
Context provider setup
type FavoritesContextType = {
favorites: Favorites;
updateFavorites: (category: StorageCategory, item: string) => void;
};
export const FavoritesContext = createContext<FavoritesContextType>(
{} as FavoritesContextType
);
const FavoritesProvider: FC = ({ children }) => {
const [favorites, setFavorites] = useState<Favorites>(
getFromLocalStorage(SOME_CONSTANT)
);
const updateFavorites = (category: StorageCategory, item: string) => {
updateLocalStorage(category, item);
setFavorites(favorites);
};
return (
<FavoritesContext.Provider value={{ favorites, updateFavorites }}>
{children}
</FavoritesContext.Provider>
);
};
export const useFavoritesContext = () => useContext(FavoritesContext);
App.tsx
export const App = () => {
return (
<FavoritesProvider>
{/* Some routing wrapper and a few routes each rendering a component */}
<Route path="/launches" element={<Launches />} />
</FavoritesProvider>
)
Launches.tsx
export const LaunchItem = ({ launch }: LaunchItemProps) => {
const { favorites, updateFavorites } = useFavoritesContext();
const [isFavorite, setIsFavorite] = useState(false);
useEffect(() => {
if (favorites) {
setIsFavorite(
favorites.launches.includes(launch.flight_number.toString())
);
}
}, [favorites]);
return (
{/* The rest of the component, irrelevant */}
<FavoriteButton
isFavorite={isFavorite}
updateFavorites={() => {
updateFavorites(
StorageCategory.Launches,
launch.flight_number.toString()
);
}}
/>
)
FavoriteButton.tsx
export const FavoriteButton = ({
isFavorite,
updateFavorites,
}: FavoriteButtonProps) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
updateFavorites();
};
return (
// Using Link vs a Button to be able to preventDefault of parent Link
<Link
onClick={handleClick}
>
{/* The rest of the component, irrelevant */}
It seems as though in your updateFavorites function you're calling setFavorites and passing in the existing favorites value. Try instead writing your updateFavorites function as:
const updateFavorites = (category: StorageCategory, item: string) => {
updateLocalStorage(category, item);
setFavorites(getFromLocalStorage(SOME_CONSTANT));
};
There are other ways you could determine what value to pass to setFavorites but I reused your getFromLocalStorage function as I'm not sure how you're determining that state value.
By doing it this way you'll ensure that the value you're setting in setFavorites isn't the same as the existing favorites value and thus you'll trigger a re-render.

How to to trigger useEffects on windows.variable Global Variable change

I am loading the two files and i have a variable called routes that i want to have changed base on a click from another component. I have a global variable called window.showhide thats boolen and another one window.route which is an object array. i want to toggle window.showhide from another component and have useEffect trigger. using window.showhide useEffects does not trigger. i just want to change window.route to route2 from another component. please help me figure this out.
import route1 from "./routes1"
import route2 from ".routes2"
window.showhide = false
window.route = route1
const routes = window.route
export default function App() {
useEffect(() => {
if(window.showhide){
routes = route2
} else {
routes = route1
}
}, [window.showhide]);
}
useEffect does not have the ability to listen for variables to change, and then cause a rerender of your component. Rather, your component has to already be rerendering due to something else, and then the dependency array decides whether or not to run the extra code found in the useEffect.
If you want to render a component in react, you need to set state. So the typical solution for your kind of problem is to have a state variable in App, and then pass down a function that lets a child set that state.
export default function App() {
const [routes, setRoutes] = useState(route1);
const showHide = useCallback((value) => {
setRoutes(value ? route1 : route2);
}, []);
return (
<ChildComponent showHide={showHide} />
);
}
// In the child:
function ChildComponent({ showHide }) {
return <div onClick={() => showHide(true)} />
}
If the components are very far removed from eachother, then passing down via props may be a pain, in which case you can use context instead:
const ShowHideContext = createContext(() => {});
export default function App() {
const [routes, setRoutes] = useState(route1);
const showHide = useCallback((value) => {
setRoutes(value ? route1 : route2);
}, []);
return (
<ShowHideContext.Provider value={showHide}>
<ChildComponent />
</ShowHideContext.Provider>
);
}
// Elsewhere:
function Example () {
const showHide = useContext(ShowHideContext);
return <button onClick={() => showHide(true)} />
}

Component not updating on state change with React Router

I'm using React and React Router. I have all my data fetching and routes defined in App.js.
I'm clicking the button in a nested child component <ChildOfChild /> which refreshes my data when clicking on a button (passed a function down with Context API) with a fetch request happening in my top component App.js (I have a console.log there so it's fetching on that click for sure). But the refreshed state of data never arrives at the <ChildOfChild /> component. Instead, it refreshes the old state. What am I doing wrong. And how can I ensure my state within <Link>is refreshing on state update.
I expect the item.name value to be updated on button click.
App component
has all the routes and data fetching
uses Reacts Context API, which I use to pass my fetching to child components
below the basic shape of the App component.
import React, {useEffect, useState} from "react";
export const FetchContext = React.createContext();
export const DataContext = React.createContext();
const App = () => {
const [data, setData] = useState([false, "idle", [], null]);
useEffect(() => {
fetchData()
}, []);
const fetchData = async () => {
setData([true, "fetching", [], null]);
try {
const res = await axios.get(
`${process.env.REACT_APP_API}/api/sample/`,
{
headers: { Authorization: `AUTHTOKEN` },
}
);
console.log("APP.js - FETCH DATA", res.data)
setData([false, "fetched", res.data, null]);
} catch (err) {
setData([false, "fetched", [], err]);
}
};
return (
<Router>
<DataContext.Provider value={data}>
<FetchContext.Provider value={fetchData}>
<Switch>
<Route exact path="/sample-page/" component={Child} />
<Route exact path="/sample-page/:id" component={ChildOfChild} />
</Switch>
</FetchContext.Provider>
</DataContext.Provider>
</Router>
)
}
Child component
import { DataContext } from "../App";
const Child = () => {
const [isDataLoading, dataStatus, data, dataFetchError] = useContext(DataContext);
const [projectsData, setProjectsData] = useState([]);
{
data.map((item) => (
<Link
to={{
pathname: `/sampe-page/${item.id}`,
state: { item: item },
}}
>
{item.name}
</Link>
));
}
Child of Child component
import { FetchContext } from "../App";
const ChildOfChild = (props) => {
const getData = useContext(FetchContext);
const [item, setItem] = useState({});
const [isItemLoaded, setIsItemLoaded] = useState(false);
useEffect(() => {
if (props.location.state.item) {
setItem(props.location.state.item);
setIsItemLoaded(true);
}
}, [props]);
return (
<div>
<button onClick={() => getData()}Refresh Data</button>
<div>{item.name}</div>
</div>
)
}
Issue
The specific data item that ChildOfChild renders is only sent via the route transition from "/sample-page/" to "/sample-page/:id" and ChildOfChild caches a copy of it in local state. Updating the data state in the DataContext won't update the localized copy held by ChildOfChild.
Suggestion
Since you are already rendering ChildOfChild on a path that uniquely identifies it, (recall that Child PUSHed to "/sample-page/${item.id}") you can use this id of the route to access the specific data item from the DataContext. There's no need to also send the entire data item in route state.
Child
Just link to the new page by item id.
<Link to={`/sampe-page/${item.id}`}>{item.name}</Link>
ChildOfChild
Add the DataContext to the component via useContext hook.
Use props.match to access the route's id match param.
import { FetchContext } from "../App";
import { DataContext } from "../App";
const ChildOfChild = (props) => {
const getData = useContext(FetchContext);
const [,, data ] = useContext(DataContext);
const [item, setItem] = useState({});
const [isItemLoaded, setIsItemLoaded] = useState(false);
useEffect(() => {
const { match: { params: { id } } } = props;
if (id) {
setItem(data.find(item => item.id === id));
setIsItemLoaded(true);
}
}, [data, props]);
return (
<div>
<button onClick={getData}Refresh Data<button />
<div>{item?.name}<div>
</div>
)
}
The useEffect will ensure that when either, or both, the data from the context or the props update that the item state will be updated with the latest data and id param.
Just a side-note about using the Switch component, route path order and specificity matter. The Switch will match and render the first component that matched the path. You will want to order your more specific paths before less specific paths. This also allows you to not need to add the exact prop to every Route. Now the Switch can attempt to match the more specific path "/sample-page/123" before the less specific path "/sample-page".
<Router>
<DataContext.Provider value={data}>
<FetchContext.Provider value={fetchData}>
<Switch>
<Route path="/sample-page/:id" component={ChildOfChild} />
<Route path="/sample-page/" component={Child} />
</Switch>
</FetchContext.Provider>
</DataContext.Provider>
</Router>
I've just rewrote your code here, I've used randomuser.me/api to fetch data
Take a look here, it has small typo errors but looks ok here
https://codesandbox.io/s/modest-paper-nde5c?file=/src/Child.js

How to add to a parent event handler in React

I have a React page setup root container page with a global Header component and some child components (via React Router, but that might not be relevant). The Header component has buttons that need to do specific things (like navigate) but also need to have functionality dictated by the child components. I have looked around for information on callbacks and props, but I am at a loss on how to achieve this. (Note, I am also using Redux but my understanding is that you should not save functions in Redux state because they are not serializable).
A simplified version of my scenario:
// Container Page
const Container = () => {
const onNavigate = () => {
// How could Cat or Dog component add extra functionality here before navigate() is called?
navigate('/complete');
};
return (
<Header onButtonClick={onNavigate}>
<Switch>
<Route path='/cats' component={Cat} />
<Route path='/dogs' component={Dog} />
</Switch>
);
}
// Cat component
const Cat = (props) => {
const speakBeforeNavigating = () => {
// This needs to happen when the "Navigate" button in the Header is clicked
console.log("Meow!");
};
return (
<span>It is a cat</span>
);
}
My recommendation is that you define all of the callbacks in the parent component, which is why I had to double check with you that the callbacks don't need to access the internal state of the child components.
I would define the props for each Route individually, and include a callback along with the props.
const ROUTES = [
{
path: "/cats",
component: Cat,
callback: () => console.log("Meow!")
},
{
path: "/dogs",
component: Dog,
callback: () => console.log("Woof!")
}
];
// Container Page
const Container = () => {
// functionality is based on the current page
const match = useRouteMatch();
// need history in order to navigate
const history = useHistory();
const onNavigate = () => {
// find the config for the current page
const currentRoute = ROUTES.find((route) => route.path === match.path);
// do the callback
currentRoute?.callback();
// navigate
history.push("/complete");
};
return (
<>
<Header onButtonClick={onNavigate} />
<Switch>
{ROUTES.map((props) => (
<Route key={props.path} {...props} />
))}
</Switch>
</>
);
};
You can use context api and send it as prop. While sending it as a prop you can pass callback function to your onNavigate function. Like this
const onNavigate = (callback) => {
callback();
navigate('/complete');
}
And you use it like this
<button onClick={() => onNavigate(() => console.log('blabla'))}
For context api information I recommend you to check React official documentation.

Resources