pass values to parent element react hooks - reactjs

Hope you can clarify this issue:
I have parent component:
function App() {
const [ thanks, setThanks ] = useState(null);
const thankerHandler=(value)=>{
setThanks(value)
}
**//in the return**
<Route component={PaymentPage} thankerHandler={thankerHandler} path="/payment" />```
and the child component :
const PaymentPage=({thankerHandler})=> {
const [thanks, setThanks] = useState(false);
**//after some logic**
useEffect(() => {
thankerHandler(thanks);
}, [thanks])
the problem is that react is telling me the following:
61 | useEffect(() => {
62 |
> 63 | thankerHandler(thanks);
64 |
65 | }, [thanks])
thankerHandler is not a function
I have no idea what I am doing wrong here, the idea is that when the function will be called in the child component, it will update the value in the parent, but it is not doing the thing, when I console log thankerHandler, just gives me the value of undefined because of the null state in the parent, if I set it there to false, gives me false and so on, but it does not recongnices it as a function.
anyone knows what I am doing wrong here?
thanks in advance

Since PaymentPage component is rendered via Route component, props passed to Route component are not passed to the PaymentPage component.
Instead of using component prop to render the PaymentPage component, use render prop which takes a function. It will allow you to pass the props to PaymentPage component
Change
<Route component={PaymentPage} thankerHandler={thankerHandler} path="/payment" />
to
<Route
path="/payment"
render={(routerProps) => <PaymentPage {...routerProps} thankerHandler={thankerHandler} />}
/>
Apart from this problem, there is another problem in PaymentPage component.
Since useEffect is calling the thankerHandler function, thankerHandler should be added to the dependency array of the useEffect hook
useEffect(() => {
thankerHandler(thanks);
}, [thanks, thankerHandler]);
but adding thankerHandler to the dependency array of useEffect hook will cause another problem, i.e. when App component re-renders, thankerHandler function will be re-created and the reference to this new function will be passed to PaymentPage component which will cause the useEffect hook in PaymentPage component to run again. This may or may not be something you want. If you don't want this, make sure to wrap thankerHanlder function in useCallback hook to prevent useEffect hook from running unnecessarily.

try to use render method in Route component:
<Route render={() => <PaymentPage thankerHandler={thankerHandler} />} path="/payment" />

Related

useffect inside <outlet> component does not trigger

Parent component:
<Main props... >
<LinksArray />
<Outlet context={investorId}/>
</Main>
outlet component
const NewBoards: React.FC = () => {
let { boardId } = useParams();
// this does not
useEffect(() => {
console.log('ue trigger page change',boardId )
}, []);
// this triggers (because of an argument
useEffect(() => {
console.log('ue trigger page change',boardId )
}, [boardId ]);
return (
<>
{console.log('>rerender')}
<p> newBoards</p>
<p>{boardId}</p>
</>
)
}
NewBoards is an outlet element, I would love for useEffect to trigger on a ROUTE (ex. boards/123 to boards/345 ) change, without passing boardId, however useEffect does not trigger on the address change unless I`m passing boardId to the dependency array. boardId param does change.
Also I would like to know why that useEffect does not trigger. I can't find anything related to it on the react router v6 official documentation
edit:
I have also noticed that states are saved. States inside outlet component( NewBoards ) do not refresh to initial ones.
edit ( router definition ) :
{
path: '/boards/',
element: authorized ? <Boards1 /> : <Login />,
children: [{
path: '/boards/:boardId',
element: authorized ? <NewBoards /> : <Login />,
}]
},
From what I see from your comments you've seriously misunderstood what is happening between the Outlet and the nested Route components that are rendering their content, i.e. element prop, into it.
Assuming authorized is true then the following route config:
{
path: '/boards/',
element: authorized ? <Boards1 /> : <Login />,
children: [{
path: '/boards/:boardId',
element: authorized ? <NewBoards /> : <Login />,
}]
},
will produce the following rendered routes:
<Routes>
...
<Route path="/boards" element={<Boards1 />}>
<Route path="/boards/:boardId" element={<NewBoards />} />
</Route>
...
</Routes>
Where I think your understanding goes awry is in thinking that when the URL path changes from "/boards/123" to "/boards/345" that the Boards1 component rendered on "/boards" will rerender and remount the Outlet. It won't. This means that the Outlet it is rendering also doesn't do anything other than output the result of the currently matched route.
A second point of confusion/misudnerstanding on your part is thinking that when the URL path changes from "/boards/123" to "/boards/345" that <Route path="/boards/:boardId" element={<NewBoards />} /> will unmount and remount a new instance of the NewBoards component. This is also not the case. The NewBoards component will remain mounted and will simply rerender. This is an optimization by react-router-dom as it's a lot more work to tear down and remount a component than it is to simply rerender it with some different props/context/etc.
The routed components using the route path params necessarily need to "listen" to changes to the params if they need to issue side-effects based on the param values. This is why the useEffect hook with empty dependency array runs only once when the NewBoards component was mounted (not each time the route changes) and the useEffect hook with the boardId param as a dependency correctly reruns each time the boardId value changes.
const NewBoards: React.FC = () => {
const { boardId } = useParams();
// Run this when component mounts, occurs once per mounting
useEffect(() => {
console.log('ue trigger page change', boardId);
}, []);
// Run this when the `boardId` param updates
useEffect(() => {
console.log('ue trigger page change', boardId);
}, [boardId]);
// Run this each render, i.e. no dependency array at all!
useEffect(() => {
console.log('>rerender');
});
return (
<>
<p>newBoards</p>
<p>{boardId}</p>
</>
);
};

React - sharing variables through useContext, to update and re-render components, but not working

I am creating an exercise app (using MERN stack and graphql)
where it takes keywords from the user and fetches youtube videos matching those keywords.
After fetching the youtube data, I save that exercise in my mongoDB database, and also saved them in an array exerciseArr. This exerciseArr is a shared through different react components, through useContext.
The fetching request and receiving data happens in my ExerciseForm component.
I am using ExerciseList component to render those fetched exercises(saved in exerciseArr), whenever there is an update in exerciseArr, it should re-render items in exerciseArr to ExerciseVideo. That is happening in useEffect of ExerciseList component.
useEffect(() => {
console.log('exerciseArr in ExerciseList', exerciseArr)
const fetchedExercises = exerciseArr.map( exercise => (
<ExerciseVideo key={exercise._id}
id={exercise._id}
bodysection={exercise.bodysection}
duration={exercise.duration}
title={exercise.title}
videoUrl={exercise.videoUrl}
favorite={exercise.favorite}/>))
setVideoElements(fetchedExercises)
}, [exerciseArr])
The issue now is that ExerciseList component only renders with 1st request from ExerciseForm, and it does not re-render, unless you initiate some action(click event etc) with the already rendered video elements which is the prat of the ExerciseList component.
My understanding is that every time there is an update of exerciseArr, useEffect in ExerciseList would re-render. But this is not happening?
To force to re-render the ExrciseList component, I need to initiate click event or any sorts, with already existing ExerciseVideo elements which is part of the ExerciseList component.
This is how I render ExerciseList component in App
....
function App() {
const { isAuth, exerciseArr } = useContext(Context)
const hasExercise = exerciseArr.length > 0
// const authenticatedView = <><ExerciseForm/></>
return (
<div className="App">
<Switch>
<Route exact path="/">
<NavBar />
<Manual />
{ isAuth ? <ExerciseForm/> : <Login/> }
{ isAuth && hasExercise ? <ExerciseList /> : ''}
</Route>
....
I wonder if I am not using useEffect in ExerciseList component correctly, in order to re-render itself. Or how I am rendering ExerciseList component in App needs to change.
Can somebody help me with this issue?
I found the error which was causing the problem.
The useEffect function is correct, but the if statement where it should render and update the exerciseArr was returning false.
if(resData.data.exercises.__typename === 'ExerciseData') {
//then save those results in exerciseArr
const graphqlResponse = resData.data.exercises.exercises
return setExerciseArr(graphqlResponse)
}
this block's resData.data.exercises.__typename path was not correct before.
Thus not updating the exerciseArr at all.
*sidenote, I moved this useEffect function into Context and only render exerciseArr in ExerciseList to create video div elements now, to make component more simple.

How to prevent infinite loop with React's useReducer, useContext and useEffect

I'm currently trying to figure out how to avoid creating an infinite loop when wrapping my application in a Context provider (taking in values from useReducer) and then updating via child component with a useEffect hook.
There's an example of the problem here on CodeSandbox.
Obviously it's difficult to talk about the problem without reposting all the code here, but key points:
Root:
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return (
<Context.Provider value={value}>
...
</Context.Provider>
Child:
export const Page1: FC = () => {
const { dispatch, state } = useContext(Context);
const { isLoading } = state;
useEffect(() => {
dispatch({
type: "loading",
payload: false
});
}, [dispatch]);
return (...)
I'm probably missing something obvious, but any pointers might help others who run into the same problem.
Full example on CodeSandbox.
The root of the problem is here
<Route path="/page1" component={() => <Page1 />} />
When you pass inlined arrow function as component you basically creating new component for every render and forcing Route to completely re-mount this part. When it happens useEffect gets called again and so on, and so on.
You need to change it like that:
<Route path="/page1"><Page1 /></Route>
// or
<Route path="/page1" component={Page1} />
Citation from react-router docs:
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).
Source: https://reactrouter.com/web/api/Route/route-render-methods

Updating Parents state from Child without triggering a rerender of Child in React

So I'm trying to build a single page app in react.
What I want:
On the page you can visit different pages like normal. On one page (index) i want a button the user can click that expands another component into view with a form. This component or form should be visible on all pages once expanded.
The Problem:
The index page loads some data from an api, so when the index component gets mounted, an fetch call is made. But when the user clicks the "Expand form"-Button, the state of the Parent component gets updated as expected, but the children get rerendered which causes the index component to fetch data again, which is not what I want.
What I tried
// Parent Component
const App => props => {
const [composer, setComposer] = useState({
// ...
expanded: false,
});
const expandComposer = event => {
event.preventDefault();
setComposer({
...composer,
expanded: true
});
return(
// ...
<Switch>
// ...
<Route
exact path={'/'}
component={() => (<Index onButtonClick={expandComposer}/>)}
// ....
{composer.expanded && (
<Composer/>
)};
);
};
// Index Component
const Index=> props => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(()=> {
// load some data
}, []);
if(isLoading) {
// show spinner
} else {
return (
// ...
<button onClick={props.onButtonClick}>Expand Composer</button>
// ...
);
};
};
So with my approach, when the button is clicked, the Index component fetched the data again and the spinner is visible for a short time. But I dont want to remount Index, or at least reload the data if possible
Two problems here. First, React will by default re render all child components when the parent gets updated. To avoid this behavior you should explicitly define when a component should update. In class based components PureComponent or shouldComponentUpdate are the way to go, and in functional components React.memo is the equivalent to PureComponent. A PureComponent will only update when one of it's props change. So you could implement it like this:
const Index = () =>{/**/}
export default React.memo(Index)
But this won't solve your problem because of the second issue. PureComponent and React.memo perform a shallow comparison in props, and you are passing an inline function as a prop which will return false in every shallow comparison cause a new instance of the function is created every render.
<Child onClick={() => this.onClick('some param')} />
This will actually create a new function every render, causing the comparison to always return false. A workaround this is to pass the parameters as a second prop, like this
<Child onClick={this.onClick} param='some param' />
And inside Child
<button onClick={() => props.onClick(props.param)} />
Now you're not creating any functions on render, just passing a reference of this.onClick to your child.
I'm not fully familiar with your style of React, I do not use them special state functions.
Why not add a boolean in the parent state, called "fetched".
if (!fetched) fetch(params, ()=>setState({ fetched: true ));
Hope this helps
Silly me, I used component={() => ...} instead of render={() => ...} when defining the route. As explained in react router docs, using component always rerenders the component. Dupocas' answer now works perfectly :)

Passing props through react router not available in componentDidMount?

I've passed props via the react router like this to the mentioned component:
<BrowserRouter>
<Switch>
<Route path="/product-page" exact render={(props) => ( <ShopPages {...props} response={this.state.response}/>)}/>
</Switch>
</BrowserRouter>
I would like to access this props in componentDidMount() like something like this:
componentDidMount() {
console.log(this.props.response)
}
The prop this.props.response is available in the component I can console.log it in the render(). I'm not sure why in the above scenario the console shows the array empty. I've tried to see if the data is available then show the data as so console.log( 'show', this.props.response && this.props.response) or by adding async as so:
async componentDidMount() {
const data = await this.props.response
}
But this does nothing too. Any help?
I'm just going to assume that this.state.response is not a Promise, but is actually the data since you said it was an empty array, therefore async/await will do nothing.
ComponentDidMount() is only fired once when the component mounts and is NOT the place to perform actions based on prop updates that can happen after mount. So I would suggest passing the promise, and if that is not an option do something like
componentDidUpdate(prevProps, prevState) {
if (!prevProps.response.length && this.props.response.length) {
//Your code
}
}

Resources