React component rerenders when sending function as props to it - javascript

I have a child component StartExam where I am sending two functions as props, from the parent component. I saw that it keeps rerendering because it gets new values of functions the whole time. I have used this piece of code to find out which props are being updated, and it gave me the two functions that I am sending.
componentDidUpdate(prevProps, prevState, snapshot) {
Object.entries(this.props).forEach(([key, val]) =>
prevProps[key] !== val && console.log(`Prop '${key}' changed`)
);
if (this.state) {
Object.entries(this.state).forEach(([key, val]) =>
prevState[key] !== val && console.log(`State '${key}' changed`)
);
}
}
This is how I am sending functions from the parent component:
<Route path={`${matchedPath}/start`}
render={
this.examStatusGuard(
'NOT_STARTED',
(props) =>
<StartExam
language={this.state.language}
startExam={() => this.startExam()}
logAction={(action) => this.logAction({action})}/>)
}
/>
and this is the examStatusGuard function:
examStatusGuard(requiredState, renderFunc) {
return (props) => {
if (this.state.exam.status !== requiredState) {
return <Redirect to={this.examStatusDefaultUrl()}/>
}
return renderFunc(props);
}
}
And this are the two functions I am sending down as props:
logAction(actionModel) {
const wholeActionModel = {
language: this.state.language,
taskId: null,
answerId: null,
...actionModel
};
console.log(wholeActionModel);
return wholeActionModel;
}
startExam() {
this.logAction({action: actions.EXAM_STARTET});
this.examGateway.startExam()
.then(() => this.loadExam())
.then(() => {
this.props.history.push("/exam/task/0");
this.logAction({action: actions.TASK_OPEN, taskId: this.state.exam.tasks[0].id});
});
};
The reason I don't want the functions to be recreated is that in the child component I have a method that calls logAction, and it is being called the whole time, instead of just once.
This is the method:
renderFirstPage() {
this.props.logAction(actions.INFOSIDE_OPEN);
return <FirstPage examInfo={this.props.eksamensInfo}>
{this.gotoNextPageComponent()}
</FirstPage>
}
I have tried with sending the functions like it is suggested in the answer, but with binding this to them:
<StartExam
language={this.state.language}
startExam={this.startExam.bind(this)}
logAction={this.logAction.bind(this)}/>
But, the functions were being recreated again the whole time.
How can I fix this?

When you send a function like you are, you are creating an anonymous function that is re-created each time the parent component renders:
startExam={() => this.startExam()}
That's an anonymous function, whose whole purpose in life is to call the actual function startExam. It's being defined right here in the render function of the parent, so it's re-created each time. You can alternatively just send that function down itself, i.e.
startExam={this.startExam}
In this case the prop now references a stable function that is not getting recreated every time. I imagine that will fix your problem.
However, it's not entirely clear to me why it matters that the function is being recreated every time and your child component is re-rendering. The props aren't changing an infinite amount of time, but rather only when the parent is rerendering. That's usually not a problem, unless you are basing some other action to see if the previous props have changed (like with lodash, _.isEqual(prevProps,this.props)).

Related

useEffect execution order between sibling components

In this code:
import React from 'react';
import './style.css';
let Child2 = () => {
React.useEffect(() => {
console.log('Child 2');
}, []);
return <div />;
};
let Child1 = ({ children }) => {
return <div>{children}</div>;
};
let FirstComponent = () => {
React.useEffect(() => {
console.log('FirstComponent');
}, []);
return <div />;
};
export default function App() {
React.useEffect(() => {
console.log('Main App');
}, []);
return (
<div>
<FirstComponent />
<Child1>
<Child2 />
</Child1>
</div>
);
}
The output is:
FirstComponent
Child 2
Main App
Question
Is there some reliable source (e.g. docs) so that we can say that always useEffect of
FirstComponent will precede useEffect of Child2?
Why is this relevant?
If we are sure that effect from FirstComponent always runs first, then it could be useful to perform some initialization work there (maybe perform some side effect), which we want to be available to all other useEffects in the app. We can't do this with normal parent/child effects, because you can see that parent effect ("Main App") runs after child effect ("Child 2").
Answering the question asked: As far as I'm aware, React doesn't guarantee the order of the calls to your component functions for the children, though it would be really surprising if they weren't in first-to-last order between siblings (so, reliably calling FirstComponent at least once before calling Child1 the first time, in that App). But although the calls to createElement will reliably be in that order, the calls to the component functions are done by React when and how it sees fit. It's hard to prove a negative, but I don't think it's documented that they'll be in any particular order.
But:
If we are sure that effect from FirstComponent always runs first, then it could be useful to perform some initialization work there, which we want to be available to all other useEffects in the app. We can't do this with normal parent/child effects, because you can see that parent effect ("Main App") runs after child effect ("Child 2".)
I wouldn't do that even if you find documentation saying the order is guaranteed. Crosstalk between sibling components is not a good idea. It means the components can't be used separately from each other, makes the components harder to test, and is unusual, making it surprising to people maintaining the codebase. You can do it anyway, of course, but as is often the case, lifting state up most likely applies ("state" in the general case, not just component state). Instead, do any one-time initialization you need to do in the parent (App) — not as an effect, but as component state in the parent, or instance state stored in a ref, etc., and pass it to the children via props, context, a Redux store, etc.
In the below, I'll pass things to the children via props, but that's just for the purposes of an example.
State
The usual place to store information used by child elements is in the parent's state. useState supports a callback function that will only be called during initialization. That's where to put this sort of thing unless you have a good reason not to. In the comments, you've suggested you have a good reason not to, but I'd be remiss if I didn't mention it in an answer meant for others in the future, not just for you now.
(This example and the second one below pass the information to the children via props, but again, it could be props, context, a Redux store, etc.)
Example:
const { useState, useEffect } = React;
let Child2 = () => {
return <div>Child 2</div>;
};
let Child1 = ({ value, children }) => {
return (
<div>
<div>value = {value}</div>
{children}
</div>
);
};
let FirstComponent = ({ value }) => {
return <div>value = {value}</div>;
};
function App() {
const [value] = useState(() => {
// Do one-time initialization here (pretend this is an
// expensive operation).
console.log("Doing initialization");
return Math.floor(Math.random() * 1000);
});
useEffect(() => {
return () => {
// This is called on unmount, clean-up here if necessary
console.log("Doing cleanup");
};
}, []);
return (
<div>
<FirstComponent value={value} />
<Child1 value={value}>
<Child2 />
</Child1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Technically, I suppose you could use it for doing something that didn't result in any value at all, but that would be odd semantically and I don't think I'd recommend it.
Non-State
If it's information that can't be stored in state for some reason, you can use a ref, either to store it (although then you might prefer state) or to just store a flag saying "I've done my initialization." One-time initialization of refs is perfectly normal. And if the initialization requires cleanup, you can do that in a useEffect cleanup callback. Here's an example (this example does end up storing something other than a flag on the ref, but it could just be a flag):
const { useRef, useEffect } = React;
let Child2 = () => {
return <div>Child 2</div>;
};
let Child1 = ({ value, children }) => {
return (
<div>
<div>value = {value}</div>
{children}
</div>
);
};
let FirstComponent = ({ value }) => {
return <div>value = {value}</div>;
};
function App() {
// NOTE: This code isn't using state because the OP has a reason
// for not using state, which happens sometimes. But without
// such a reason, the normal thing to do here would be to use state,
// perhaps `useState` with a callback function to do it once
const instance = useRef(null);
if (!instance.current) {
// Do one-time initialization here, save the results
// in `instance.current`:
console.log("Doing initialization");
instance.current = {
value: Math.floor(Math.random() * 1000),
};
}
const { value } = instance.current;
useEffect(() => {
return () => {
// This is called on unmount, clean-up here if necessary
console.log("Doing cleanup");
};
}, []);
return (
<div>
<FirstComponent value={value} />
<Child1 value={value}>
<Child2 />
</Child1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Your specific example use case
Re the specific use case you linked (note: the code from the question may not be correct; I'm not trying to correct it here, not least because I don't use axios):
I am using an axios interceptor to handle errors globally, but would like to set the state of my app from the interceptor.
axios.interceptors.response.use(
error => {
AppState.setError(error)
}
)
(And you've indicated that AppState, whatever it is, is only accessible within App.)
I'm not a fan of modifying the global axios instance, it's another crosstalky thing that affects all code using axios in the page/app, regardless of whether it's your code or code in a library that may use axios in a way where it's not appropriate for your app to show an error state that occurs.
I'd lean toward decoupling the interceptor from the App state update:
Have an axios wrapper module taht exports a custom axios instance
Put the interceptor on that instance
Provide a means of subscribing to error events from that module
Have App subscribe to the error event from that to set its state (and unsubscribe on unmount)
That sounds complicated, but it's only about 30 lines of code even if you don't have a prebuilt event emitter class:
import globalAxios from "axios";
const errorListeners = new Set();
export function errorSubscribe(fn) {
errorListeners.add(fn);
}
export function errorUnsubscribe(fn) {
errorListeners.delete(fn);
}
function useErrorListener(fn) {
const subscribed = useRef(false);
if (!subscribed.current) {
subscribed.current = true;
errorSubscribe(fn);
}
useEffect(() => {
return () => errorUnsubscribe(fn);
}, []);
}
export const axios = globalAxios.create({/*...config...*/});
instance.interceptors.response.use(() => {
(error) => {
for (const listener of errorListeners) {
try { listener(error); } catch {}
}
};
});
Then in App:
import { axios, useErrorListener } from "./axios-wrapper";
function App() {
useErrorListener((error) => AppState.setError(error));
// ...
}
In code that needs to use that instance of axios:
import { axios } from "./axios-wrapper";
// ...
That's a bit barebones (it assumes you'd never have dependencies on the error callback function, for instance), but you get the idea.
I second to #T.J. Crowder, you should not rely on execution order of components to implement any feature. For reasons:
What you want to achieve is anti-pattern, implicit dependency that surprises ppl. JUST DON'T DO IT.
It's not very reliable after all. The execution order is maintained, but continuity is not guaranteed.
I'll complement his answer with some details on execution order of React components.
So for a simple case of:
function A() {
return (<>
<B />
<C>
<D />
</C>
</>
);
}
The rule of thumb to determine component execution order, is to think in terms of JSX element creation call. In our case it'll be A(B(), C(D())), Same as JS function execution order, the execution (or "render") order of components would be B, D, C, A.
But this comes with caveats:
If any component bails out, e.g., D is a React.memo'ed "pure" component and its props doesn't change, then order would be B, C, A, order is maintained, but bailout component is skipped.
Not-so-everyday exception like SuspenseList (experimental)
<SuspenseList> coordinates the “reveal order” of the closest <Suspense> nodes below it.
which of cause affects execution order of its children by design.
In concurrent mode, because rendering can be interrupted at React's discretion, the continuity of execution order is in question. Sth like B, D, B, D, C, A could happen. (That said, useEffect seems unaffected AFAICT cus it's invoked in commit phase)

Child component's render still evaluated even though parent returns different component

I'm using swr in a react project and I'm trying to generify the loading/error messages in a parent component wrapping the components loading data.
The wrapping component is a very simple component returning different messages depending on the loadingstate.
const LoadingView = ({ loading, error, children }) => {
if (error) {
return <span>Error</span>
}
if (loading) {
return <span>Loading...</span>
}
return <Container>{children}</Container>
}
And the child component:
const WipePeriodTeams = ({ wipePeriodId }) => {
const params = useParams()
const { data, error } = useSWR(
`/some-endpoint`
)
return <LoadingView loading={!data}>{console.log(data.length)}</LoadingView> <--- ReferenceError
}
The issue being that the child component's render method is always evaluated, doesn't matter if loading is true/false which could end up in a ReferenceError due to data not loaded.
Is the return value always evaluated no matter what the parent returns? Is there a way around this?
Thanks! :)
That is the correct behaviour - the evaluation of children occurs in the parent component. You are seeing an error because data is undefined, so data.length is trying to point to a property of something that doesn't exist.
One way to avoid the error is to use && separator to check if data exists before referring to its length:
<LoadingView loading={!data}>{data && console.log(data.length)}</LoadingView>
Another approach is to replace your JSX expression with a component. I understand your example has a contrived child console.log(), but in the real world you're likely to pass in another component(s). Components are functions, so not evaluated at parent level:
const ChildComponent = ({data}) => {
return (<>{console.log(data.length)}</>)
}
const Parent = () => {
const { data, error } = useSWR(
`/some-endpoint`
);
return (
<LoadingView loading={!data}>
<ChildComponent data={data} />
</LoadingView>
);
}
Live example
There'a a few other approaches to delaying evaluation of children if you dig around online, but be cautious as some of them feel like messy workarounds.

React functional components in Array.map are always rerendering when getting passed a function as props

I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState and passes it as props to the childs. Additionally the onClick function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
... useCallback to make react see that the onClick function always stays the same
... React.Fragment to make react find a component again because otherwise a child would not have a unique id or sth similar
The child component is exported as:
export default React.memo(TheChildComponent, compareEquality) with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality is never executed and therefore I know that compareEquality is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick function and by doing that updated the isSelected state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick function every render, because useCallback isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo, this should then prevent the children from re-rendering except when isSelected changes. (Your second argument to React.memo should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map).
Turns out the only problem was neither useCallback, useMemo or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.

How do I see what props have changed in React?

I am looking to make some performance improvements to my React components.
If I have a component that is called like:
<MyComponent
anObject={someObject}
aFn={someFn}
aValue={value}
/>
React will only "re-render" that component if anObject, aFn, or aValue has changed. If I know my function is re-rendering, how can I tell which of those three props caused it?
I know I can console.log aValue and see if it changes, but what about the object and the function. They can have the same values but be referencing different memory. Can I output the memory location?
You can use a useEffect hook for each prop:
const MyComponent = ({ anObject, aFn, aValue }) => {
React.useEffect(() => {
console.log('anObject changed');
}, [anObject]);
React.useEffect(() => {
console.log('aFn changed');
}, [aFn]);
React.useEffect(() => {
console.log('aValue changed');
}, [aValue]);
// Your existing component code...
};
When the component mounts all three effects will run, but after that the logs will reveal what specifically is changing.

React setState re-render

First of all, I'm really new into React, so forgive my lack of knowledge about the subject.
As far as I know, when you setState a new value, it renders again the view (or parts of it that needs re-render).
I've got something like this, and I would like to know if it's a good practice or not, how could I solve this kind of issues to improve, etc.
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
key: value
}
this.functionRender = this.functionRender.bind(this)
this.changeValue = this.changeValue.bind(this)
}
functionRender = () => {
if(someParams !== null) {
return <AnotherComponent param={this.state.key} />
}
else {
return "<span>Loading</span>"
}
}
changeValue = (newValue) => {
this.setState({
key: newValue
})
}
render() {
return (<div>... {this.functionRender()} ... <span onClick={() => this.changeValue(otherValue)}>Click me</span></div>)
}
}
Another component
class AnotherComponent extends Component {
constructor (props) {
super(props)
}
render () {
return (
if (this.props.param === someOptions) {
return <div>Options 1</div>
} else {
return <div>Options 2</div>
}
)
}
}
The intention of the code is that when I click on the span it will change the key of the state, and then the component <AnotherComponent /> should change because of its parameter.
I assured that when I make the setState, on the callback I throw a console log with the new value, and it's setted correctly, but the AnotherComponent doesn't updates, because depending on the param given it shows one thing or another.
Maybe I need to use some lifecycle of the MyComponent?
Edit
I found that the param that AnotherComponent is receiving it does not changes, it's always the same one.
I would suggest that you'll first test it in the parent using a simple console.log on your changeValue function:
changeValue = (newValue) => {
console.log('newValue before', newValue);
this.setState({
key: newValue
}, ()=> console.log('newValue after', this.state.key))
}
setState can accept a callback that will be invoked after the state actually changed (remember that setState is async).
Since we can't see the entire component it's hard to understand what actually goes on there.
I suspect that the newValue parameter is always the same but i can't be sure.
It seems like you're missing the props in AnotherComponent's constructor. it should be:
constructor (props) {
super(props) // here
}
Try replacing the if statement with:
{this.props.param === someOptions? <div>Options 1</div>: <div>Options 2</div>}
also add this function to see if the new props actually get to the component:
componentWillReceiveProps(newProps){
console.log(newProps);
}
and check for the type of param and someOptions since you're (rightfully) using the === comparison.
First, fat arrow ( => ) autobind methods so you do not need to bind it in the constructor, second re-renders occur if you change the key of the component.
Ref: https://reactjs.org/docs/lists-and-keys.html#keys

Categories

Resources