How to add to a parent event handler in React - reactjs

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.

Related

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

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.

Simulate user navigation with React Testing Library and React Router

I have a component that is meant to trigger when the user navigates away from a page. It wraps a formik form; if a user has unsaved changes, it attempts to save those changes as soon as the user attempts to navigate away. While the save is resolving, users will see a modal that says "saving..." If the save is successful, the user continues on to the next page. If it is unsuccessful, it displays a modal prompting them to either stay or move along. The component works fine, but I'm struggling to test it.
Component in question:
const AutoSaveUnsavedChangesGuard: React.FC<Props> = ({
when,
onLeave,
children,
ignoreRoutes = [],
submitForm,
}) => {
const { push } = useHistory();
const { error, savingStatus } = useSavingStatusContext();
const [nextLocation, setNextLocation] = React.useState<string>();
const [isShowing, setIsShowing] = React.useState<boolean>(false);
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = React.useState<boolean>(false);
const [showSavingModal, setShowSavingModal] = React.useState<boolean>(false);
const handleBlockNavigation = (nextLocation: Location) => {
if (!!matchPath(nextLocation.pathname, ignoreRoutes)) {
return true;
}
setNextLocation(nextLocation.pathname);
setIsShowing(true);
submitForm();
return false;
};
React.useEffect(() => {
// Proceed to next location when there has been a navigation attempt and client no longer blocks it
if (!when && nextLocation) {
push(nextLocation);
}
}, [when, nextLocation, push]);
React.useEffect(() => {
// If we have an error and we have triggered the Prompt, display the unsaved changes guard.
setShowUnsavedChangesModal(!!error)
}, [error]);
React.useEffect(() => {
setShowSavingModal(savingStatus=== SavingStatusType.SAVING)
}, [savingStatus]);
return (
<React.Fragment>
<Prompt when={when} message={handleBlockNavigation}/>
<UnsavedChangesModal
show={showUnsavedChangesModal && isShowing}
onLeave={() => {
onLeave && onLeave();
}}
onStay={() => {
setNextLocation(undefined);
}}
onHide={() => {
setIsShowing(false);
setShowUnsavedChangesModal(false);
}}
/>
<SavingModal show={showSavingModal && isShowing} />
{children}
</React.Fragment>
);
};
export default AutoSaveUnsavedChangesGuard;
I'm trying to test behavior with react-testing-library. I'd like to simulate a user navigating away (IE call the message method in the rendered component with a new location), but I am struggling to do so. We had a function like the one below when we tested using enzyme.
const changeRouteLocation = (nextLocation: Location, wrapper: ShallowWrapper) => {
const prompt = wrapper.find(ReactRouter.Prompt);
const onNavigate = prompt.props().message as (location: Location) => string | boolean;
onNavigate(nextLocation);
};
Unfortunately, this component uses useEffect hooks that don't play nice with enzyme, and I must test it using react-testing-library. How can I simulate a user attempting to navigate to a new location with react-testing-library?
Edit: Adding what I have for testing code per a request. This code does not produce the desired outcome, and I honestly didn't expect it to.
const RenderingComponent = ({initialEntries})=>{
return(
<ThemeProvider>
<MemoryRouter initialEntries={initialEntries}>
<AutoSaveUnsavedChangesGuard {...defaults} />
</MemoryRouter>
</ThemeProvider>
)
}
beforeEach(() => {
jest.spyOn(ReactRouter, 'useHistory').mockReturnValue(makeHistory());
useSavingStatusContextSpy = jest.spyOn(useAutoSaveContextModule, 'useSavingStatusContext')
});
it('should render default. It should not show any modals when there are no errors and the route has not changed.', async () => {
// Default rendering. Works fine, because it's not meant to display anything.
const wrapper = render(
<RenderingComponent initialEntries={['/initial-value']} />
)
expect(screen.queryByText('Saving...')).toBeNull();
expect(screen.queryByText('Unsaved Changes')).toBeNull();
expect(wrapper).toMatchSnapshot()
});
it('should show the saving modal when the route location changes and saving status context is of type SAVING',()=>{
useSavingStatusContextSpy.mockReturnValueOnce(makeAutoSaveContext({savingStatus: SavingStatusType.SAVING}))
const {rerender, debug} = render(
<RenderingComponent initialEntries={["initial-value"]} />
)
rerender(<RenderingComponent initialEntries={['/mock-value','/mock-some-new-value']} />)
// I had hoped that re-rendering with new values for initial entries would trigger a navigation event for the prompt to block. It did not work.
debug()
const savingModal = screen.getByText('Saving...');
expect(savingModal).toBeVisible();
})
})

Prevent top scroll after update state

Hi I have problem with scroll to top page after every render.
In my Root component, when I get Array of objects items from Redux Store.
I filter mainArray on three subsArrays like : Hot, Favorite, Regular, then render each of them on specific route.
The filter func is running each time when mainArray is updated: like rating is rise and down or set favorite will be marked.
The question is, why react render each times when action is dispatching to redux store(I think redux causes this,I guess) and how I can prevent this.
Please give me a hint, I struggle with it for a while...
function Root() {
const state = useSelector((state) => state);
const { mainList: list } = state;
const [regularList, setRegularList] = useState([]);
const [hotList, setHotList] = useState([]);
const [favoriteList, setFavoriteList] = useState([]);
const dispatch = useDispatch();
const handleVote = (e) => {
const currentId = Number(e.nativeEvent.path[3].id);
const name = e.currentTarget.dataset.name;
dispatch(listActions.vote(currentId, name));
};
const handleSetFave = (e) => {
const currentId = Number(e.nativeEvent.path[3].id);
dispatch(listActions.setFave(currentId));
const setArrays = (arr) => {
const regularArr = arr.filter((meme) => meme.upvote - meme.downvote <= 5);
const hotArr = arr.filter((meme) => meme.upvote - meme.downvote > 5);
const favoriteArr = arr.filter((meme) => meme.favorite);
setRegularList([...regularArr]);
setHotList([...hotArr]);
setFavoriteList([...favoriteArr]);
};
useEffect(() => {
window.localStorage.getItem("mainList") &&
dispatch(
listActions.setMainList(
JSON.parse(window.localStorage.getItem("mainList"))
)
);
}, []);
useEffect(() => {
setArrays(list);
list.length > 0 &&
window.localStorage.setItem("mainList", JSON.stringify(list));
}, [list]);
return (
<div className={styles.App}>
<Router>
<Navigation />
<Route path="/" component={FormView} exact />
<Route
path="/regular"
component={() => (
<MemesView
list={regularList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
<Route
path="/hot"
component={() => (
<MemesView
list={hotList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
<Route
path="/favorite"
component={() => (
<MemesView
list={favoriteList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
</Router>
</div>
);
};
why react render each times when action is dispatching to redux store
Because you are subscribing to the whole redux state (by using useSelector((state) => state);), remember that each time an action is dispatched, redux computes a new state.
So you should not write const state = useSelector(state => state); otherwise your component will be rerendered each time an action is dispatched. Instead you must select the part of the state you are interested in.
I can deduce from your code you want to be notified every time there is a change on the mainList, so you can write :
const list = useSelector(state => state.mainList);
You can get more info by reading the documentation
by default useSelector() will do a reference equality comparison of the selected value when running the selector function after an action is dispatched, and will only cause the component to re-render if the selected value changed
Basically, the scroll to top page you are experiencing might also comes from a bad use of the Route component.
Try to use this
<Route path="/regular">
<MemesView
list={regularList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
</Route>
instead of
<Route path="/regular"
component={() => (
<MemesView
list={regularList}
handleVote={handleVote}
handleSetFave={handleSetFave}
/>
)}
/>
Don't forget to also update /hot and /favorite routes.
You can read from the react router documentation
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).

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

React setting Stateful variables in map function

I am trying to set up a react app where a list of buttons are displayed, the user can press a button and be taken to a page with information about a country. I am creating the buttons programmatically using a .map function. I am using a SQL database to store country names, and information about the countries, and then calling a flask route to pull the data into my react app. For that, I am using an async function.
This is the process that I would like to have happen:
I set up some stateful variables in my App.js main router component. I then pass as props my setState functions to my component with the buttons and the .map function. For each button, there is the option to set the state of the variables in the App.js component. I would then set the variables in App.js to the values associated with the button clicked. From there, I could pass those stateful variables to my country page component for display.
What actually happens:
I pass the props to my country component, expecting a country and country details to pass along with it, but I end up getting undefined. It looks like undefined might be the last element of the dataset, as I have gotten Zimbabwe as the result before. Here is my code for the App.js router:
export default function App() {
const [cname, setCName] = useState('')
const [pdf, setPdf] = useState('')
const [details, setDetails] = useState('')
return (
<div className="App">
<BrowserRouter>
{/* <Route exact path="/" component = { Home }/> */}
<Route path="/cia" component = {(props) => <CIALanding {...props} setCName={setCName} setPdf={setPdf} setDetails={setDetails}/>}/>
<Route path="/country" component={(props) => <Country {...props} setCName={setCName} details={details} cname={cname}/>}/>
<Route path="/countrypage" component={CountryPage}/>
</BrowserRouter>
</div>
);
}
Here is the code for my landing page (with the .map function)
export default function CIALanding(props) {
const [countriesList, setCountriesList] = useState([])
const getCountries = async () => {
const response = await fetch('http://127.0.0.1:5000/countries');
const data = await response.json();
setCountriesList(data['country_list'].map((country) => {return (
<Link to={{pathname:'/country',
}}>
<Country cname1={country[0]} details={country[2]} setCName={props.setCName}>{country[0]}</Country>
</Link>
)}))
}
useEffect(() => {
getCountries()
},[])
return (
<div>
{countriesList}
</div>
)
}
Here is my code for the Country Component
export default function Country(props) {
return (
<div>
{console.log(props.cname)}
<Button onClick={props.setCName(props.cname1)}>{props.cname1}</Button>
</div>
)
}
Thanks a lot for the help!
I will not exactly anwser to your question but I propose some refactoring and maybe that will solve your problem.
Firstly I will move fetching code to the App component, it will allow easier access to this data by components (I added some nice handling of fetching status change). Here you will render proper Routes only if data is fetched successfully.
const App = () => {
const [status, setStatus] = useState(null);
const [countries, setCountries] = useState([]);
const getCountries = async () => {
setStatus('loading');
try {
const response = await fetch('http://127.0.0.1:5000/countries');
const data = await response.json();
setCountriesList([...data['country_list']]);
setStatus('success')
} catch (error) {
setSatus('error');
}
}
useEffect(() => {
getCountries();
}, [])
if (!status || status === 'error') {
return <span>Loading data error</span>
}
if (status === 'loading') {
return <span>Loading...</span>
}
return (
<div className="App">
<BrowserRouter>
<Route path="/cia" component={(props) => <CIALanding {...props} countries={countries} />
<Route path="/country/:countryId" component={(props) => <Country {...props} countries={countries} />
</BrowserRouter>
</div>
);
}
Second thing - to display proper country page you don't need to set any data into state, only thing you need is to set route /country/:countryId and Links with proper paths where countryId can be unique country identyficator as number or code. With setup like this only data needed in component is array of countries and which country is loaded is decided by routing
Landing component will be nice and simple (you definitely shouldn't keep React components in state, only data)
const CIALanding = ({countries}) => (
<div>
{
countries.map(({countryName, countryId}) => (
<Link to={`/country/${countryId}`}>{countryName}</Link>
))
}
</div>
)
So now we have nice list of countries with proper links. And then country page will know which data to display by param countryId
const Country = ({match, countries}) => {
//match object is passed by Route to this component and inside we have params object with countryId
const {countryId} = match.params;
const country = countries.find(country => country.countryId === countryId);
if (country) {
return (
<div>
Show info about selected country
</div>
);
}
return (
<div>
Sorry, cannot find country with id {countryId}
</div>
)
}
And you can access proper country page by clicking on Link and additionally by entering path for example .../country/ENG in browser (I don't know your data structure so remeber to use correct data for countryId) ;)
Sorry if this don't resolve your problems but I hope it contains at least some nice ideas for refactoring ;)

Resources