How do I see what props have changed in React? - javascript

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.

Related

how to stop re-rendering of child component if parent update the context or state in react js?

how to stop re-rendering of child component if parent update the context or state in react js ?
I am already using React.memo still it is re-rendering.
this is my child component
const Ab = () => {
console.log("---ff-ddd");
const pageContext = useContext(PageContext);
useEffect(() => {
setTimeout(() => {
pageContext.updateGlobalMenu({});
}, 5000);
}, []);
return <div>ddd</div>;
};
export default React.memo(Ab);
I am updating the context. I want it update the context value but not re-render the child component
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = (menuContent) => {
setGlobalMenu(menuContent);
};
return (
<PageContext.Provider
value={{
updateGlobalMenu
}}
>
<Ab />
</PageContext.Provider>
);
}
here is my code
https://codesandbox.io/s/friendly-bartik-3cnqvf?file=/pages/index.js:156-470
if you see it print two times console. it means it is re-rendering two times
If PageContext's value changes, then any component consuming that context (including Ab) will render. React.memo cannot stop this. In your case you're changing the value on every render, so you have room to improve it by memoizing the context value. That way, the value won't change unless it needs to:
export default function IndexPage() {
const [globalMenu, setGlobalMenu] = useState("");
const updateGlobalMenu = useCallback((menuContent) => {
setGlobalMenu(menuContent);
}, []);
const value = useMemo(() => ({
updateGlobalMenu
}), [updateGlobalMenu]);
return (
<PageContext.Provider value={value}>
<Ab />
</PageContext.Provider>
);
}
You can also, in addition to memoisation from the previous answer, split your "api" and "data" portion of the state into two different providers.
updateGlobalMenu will be in PageContextAPI provider, globalMenu will be in PageContextData provider. That way when you update the data, only the provider with the data will be re-rendered.
Take a look at this article, I covered this technique in detail here:https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context

How to subscribe on updates within ReactReduxContext.Consumer?

I would like to figure out how to subscribe on updates of a stored value it the redux store.
So far I've tried something like the following:
<ReactReduxContext.Consumer>
{({store}) => {
console.log('store:', store.getState());
const p = <p>{store.getState().value}</p>;
store.subscribe(() => {p.innerText = store.getState().value});
return p;
}}
</ReactReduxContext.Consumer>
bumping into the TypeError: can't define property "innerText": Object is not extensible error on updates.
So I wonder how to update the contents?
There are a few things about your code that are just not the way that we do things in React.
React is its own system for interacting with the DOM, so you should not attempt direct DOM manipulation through .innerText. Your code doesn't work because the variable p which you create is a React JSX Element rather than a raw HTML paragraph element, so it doesn't have properties like innerText.
Instead, you just return the correct JSX code based on props and state. The code will get updated any time that props or state change.
The ReactReduxContext is used internally by the react-redux package. Unless you have a good reason to use it in your app, I would not recommend it. There are two built-in ways that you can get a current value of state that is already subscribed to changes.
useSelector hook
(recommended)
export const MyComponent1 = () => {
const value = useSelector(state => state.value);
return <p>{value}</p>
}
connect higher-order component
(needed for class components which cannot use hooks)
class ClassComponent extends React.Component {
render() {
return <p>{this.props.value}</p>
}
}
const mapStateToProps = state => ({
value: state.value
});
const MyComponent2 = connect(mapStateToProps)(ClassComponent)
ReactReduxContext
(not recommended)
If anyone reading this has a good reason why they should need to use store.subscribe(), proper usage would look something like this:
const MyComponent3 = () => {
const { store } = useContext(ReactReduxContext);
const [state, setState] = useState(store.getState());
useEffect(() => {
let isMounted = true;
store.subscribe(() => {
if (isMounted) {
setState(store.getState());
}
});
// cleanup function to prevent calls to setState on an unmounted component
return () => {
isMounted = false;
};
}, [store]);
return <p>{state.value}</p>;
};
CodeSandbox Demo

React Warning: Cannot update a component from inside the function body of a different component

I am using Redux with Class Components in React. Having the below two states in Redux store.
{ spinner: false, refresh: false }
In Parent Components, I have a dispatch function to change this states.
class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
this.props.onShowSpinner();
this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
In Child Component, I am trying to reload the parent component like below.
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
if (somecondition) {
// doing some redux store update
props.reloadApp();
}
}
render() {
return <button />;
}
}
I am getting error as below.
Warning: Cannot update a component from inside the function body of a
different component.
How to remove this warning? What I am doing wrong here?
For me I was dispatching to my redux store in a React Hook. I had to dispatch in a useEffect to properly sync with the React render cycle:
export const useOrderbookSubscription = marketId => {
const { data, error, loading } = useSubscription(ORDERBOOK_SUBSCRIPTION, {
variables: {
marketId,
},
})
const formattedData = useMemo(() => {
// DISPATCHING HERE CAUSED THE WARNING
}, [data])
// DISPATCHING HERE CAUSED THE WARNING TOO
// Note: Dispatching to the store has to be done in a useEffect so that React
// can sync the update with the render cycle otherwise it causes the message:
// `Warning: Cannot update a component from inside the function body of a different component.`
useEffect(() => {
orderbookStore.dispatch(setOrderbookData(formattedData))
}, [formattedData])
return { data: formattedData, error, loading }
}
If your code calls a function in a parent component upon a condition being met like this:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
Try wrapping the condition in a useEffect:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
useEffect(() => {
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
}, [data, handleNoUsersLoaded]);
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
It seems that you have latest build of React#16.13.x. You can find more details about it here. It is specified that you should not setState of another component from other component.
from the docs:
It is supported to call setState during render, but only for the same component. If you call setState during a render on a different component, you will now see a warning:
Warning: Cannot update a component from inside the function body of a different component.
This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the setState call into useEffect.
Coming to the actual question.
I think there is no need of getDerivedStateFromProps in the child component body. If you want to trigger the bound event. Then you can call it via the onClick of the Child component as i can see it is a <button/>.
class Child extends React.Component {
constructor(props){
super(props);
this.updateState = this.updateState.bind(this);
}
updateState() { // call this onClick to trigger the update
if (somecondition) {
// doing some redux store update
this.props.reloadApp();
}
}
render() {
return <button onClick={this.updateState} />;
}
}
Same error but different scenario
tl;dr wrapping state update in setTimeout fixes it.
This scenarios was causing the issue which IMO is a valid use case.
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setSomeState(someNewValue);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
fix
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setTimeout(() => {
setSomeState(someNewValue);
}, 0);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
In my case I had missed the arrow function ()=>{}
Instead of onDismiss={()=>{/*do something*/}}
I had it as onDismiss={/*do something*/}
I had same issue after upgrading react and react native, i just solved that issue by putting my props.navigation.setOptions to in useEffect. If someone is facing same problen that i had i just want to suggest him put your state changing or whatever inside useEffect
Commented some lines of code, but this issue is solvable :) This warnings occur because you are synchronously calling reloadApp inside other class, defer the call to componentDidMount().
import React from "react";
export default class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
// this.props.onShowSpinner();
// this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
// if (somecondition) {
// doing some redux store update
props.reloadApp();
// }
}
componentDidMount(props) {
if (props) {
props.reloadApp();
}
}
render() {
return <h1>This is a child.</h1>;
}
}
I got this error using redux to hold swiperIndex with react-native-swiper
Fixed it by putting changeSwiperIndex into a timeout
I got the following for a react native project while calling navigation between screens.
Warning: Cannot update a component from inside the function body of a different component.
I thought it was because I was using TouchableOpacity. This is not an issue of using Pressable, Button, or TouchableOpacity. When I got the error message my code for calling the ChatRoom screen from the home screen was the following:
const HomeScreen = ({navigation}) => {
return (<View> <Button title = {'Chats'} onPress = { navigation.navigate('ChatRoom')} <View>) }
The resulting behavior was that the code gave out that warning and I couldn't go back to the previous HomeScreen and reuse the button to navigate to the ChatRoom. The solution to that was doing the onPress in an inline anonymous function.
onPress{ () => navigation.navigate('ChatRoom')}
instead of the previous
onPress{ navigation.navigate('ChatRoom')}
so now as expected behavior, I can go from Home to ChatRoom and back again with a reusable button.
PS: 1st answer ever in StackOverflow. Still learning community etiquette. Let me know what I can improve in answering better. Thanx
If you want to invoke some function passed as props automatically from child component then best place is componentDidMount lifecycle methods in case of class components or useEffect hooks in case of functional components as at this point component is fully created and also mounted.
I was running into this problem writing a filter component with a few text boxes that allows the user to limit the items in a list within another component. I was tracking my filtered items in Redux state. This solution is essentially that of #Rajnikant; with some sample code.
I received the warning because of following. Note the props.setFilteredItems in the render function.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- Danger! Updates Redux during a render!
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I ran this code with React 16.12.0, I received the warning listed in the topic of this thread in my browser console. Based on the stack trace, the offending line was my props.setFilteredItems invocation within the render function. So I simply enclosed the filter invocations and state change in a useEffect as below.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
useEffect(() => {
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- OK now; effect runs outside of render.
}, [nameFilter, cityFilter]);
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I first added the useEffect I blew the top off the stack since every invocation of useEffect caused state change. I had to add an array of skipping effects so that the effect only ran when the filter fields themselves changed.
I suggest looking at video below. As the warning in the OP's question suggests, there's a change detection issue with the parent (Parent) attempting to update one child's (Child 2) attribute prematurely as the result of another sibling child's (Child 1) callback to the parent. For me, Child 2 was prematurely/incorrectly calling the passed in Parent callback thus throwing the warning.
Note, this commuincation workflow is only an option. I personally prefer exchange and update of data between components via a shared Redux store. However, sometimes it's overkill. The video suggests a clean alternative where the children are 'dumb' and only converse via props mand callbacks.
Also note, If the callback is invoked on an Child 1 'event' like a button click it'll work since, by then, the children have been updated. No need for timeouts, useEffects, etc. UseState will suffice for this narrow scenario.
Here's the link (thanks Masoud):
https://www.youtube.com/watch?v=Qf68sssXPtM
In react native, if you change the state yourself in the code using a hot-reload I found out I get this error, but using a button to change the state made the error go away.
However wrapping my useEffect content in a :
setTimeout(() => {
//....
}, 0);
Worked even for hot-reloading but I don't want a stupid setTimeout for no reason so I removed it and found out changing it via code works just fine!
I was updating state in multiple child components simultaneously which was causing unexpected behavior. replacing useState with useRef hook worked for me.
Try to use setTimeout,when I call props.showNotification without setTimeout, this error appear, maybe everything run inTime in life circle, UI cannot update.
const showNotifyTimeout = setTimeout(() => {
this.props.showNotification();
clearTimeout(showNotifyTimeout);
}, 100);

How to set initial state value for useState Hook in jest and enzyme?

I know this question is already asked here: How to set initial state for useState Hook in jest and enzyme?
const [state, setState] = useState([]);
And I totally agree with Jimmy's Answer to mock the useState function from test file but I have some extended version of this question, "What if I have multiple useState statements into the hooks, How can I test them and assign the respective custom values to them?"
I have some JSX rendering with the condition of hook's state values and depending on the values of that state the JSX is rendering.
How Can I test those JSX by getting them into the wrapper of my test case code?
Upon the answer you linked, you can return different values for each call of a mock function:
let myMock = jest.fn();
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
In my opinion this is still brittle. You may modify the component and add another state later, and you would get confusing results.
Another way to test a React component is to test it like a user would by clicking things and setting values on inputs. This would fire the event handlers of the component, and React would update the state just as in real configuration. You may not be able to do shallow rendering though, or you may need to mock the child components.
If you prefer shallow rendering, maybe read initial state values from props like this:
function FooComponent({initialStateValue}) {
const [state, setState] = useState(initialStateValue ?? []);
}
If you don't really need to test state but the effects state has on children, you should (as some comments mention) just then test the side effect and treat the state as an opaque implementation detail. This will work if your are not using shallow rendering or not doing some kind of async initialization like fetching data in an effect or something otherwise it could require more work.
If you cant do the above, you might consider removing state completely out of the component and make it completely functional. That way any state you need, you can just inject it into the component. You could wrap all of the hooks into a 'controller' type hook that encapsulates the behavior of a component or domain. Here is an example of how you might do that with a <Todos />. This code is 0% tested, its just written to show a concept.
const useTodos = (state = {}) => {
const [todos, setTodos] = useState(state.todos);
const id = useRef(Date.now());
const addTodo = useCallback((task) => {
setTodos((current) => [...current, { id: id.current++, completed: false, task }]);
}, []);
const removeTodo = useCallback((id) => {
setTodos((current) => current.filter((t) => t.id !== id));
}, []);
const completeTodo = useCallback((id) => {
setTodos((current) => current.map((t) => {
let next = t;
if (t.id === id) {
next = { ...t, completed: true };
}
return next;
}))
}, []);
return { todos, addTodo, removeTodo, completeTodo };
};
const Todos = (props) => {
const { todos, onAdd, onRemove, onComplete } = props;
const onSubmit = (e) => {
e.preventDefault();
onAdd({ task: e.currentTarget.elements['todo'].value });
}
return (
<div classname="todos">
<ul className="todos-list">
{todos.map((todo) => (
<Todo key={todo.id} onRemove={onRemove} onComplete={onComplete} />
)}
</ul>
<form className="todos-form" onSubmit={onSubmit}>
<input name="todo" />
<button>Add</button>
</form>
</div>
);
};
So now the parent component injects <Todos /> with todos and callbacks. This is useful for testing, SSR, ect. Your component can just take a list of todos and some handlers, and more importantly you can trivially test both. For <Todos /> you can pass mocks and known state and for useTodos you would just call the methods and make sure the state reflects what is expected.
You might be thinking This moves the problem up a level and it does, but you can now test the component/logic and increase test coverage. The glue layer would require minimal if any testing around this component, unless you really wanted to make sure props are passed into the component.

Creating a React higher order component, to serve as a "loader"(animation) wrapper for child components

I have a lot of components, that require some ajax function being sent, in the componentDidMount method. I would like to create a HOC, whose sole purpose is to "apply" some animation to the component, and stop this animation once a certain promise is resolved.
Of course, i could just copy paste this code for each component, but i would like to create some abstraction that deals with it.
The problem is, that i don't know how to pass the function properly, from the child to the parent. For instance, let's assume the intended child component, has this componentDidMount:
componentDidMount() {
ajax('/costumers')
.then(({ data }) => {
this.setState(() => ({ costumers: data.content }))
})
}
Technically, i need to either pass this function as an argument to the HOC, or perhaps somehow "hijack" the child's componentDidMount(if something like that is possible...). The HOC would then apply an animation once it's loaded, then send the ajax, and only when it's solved, the animation is eliminated, and the child component gets rendered.
How can this be achieved?
Any idea will be appreciated
Here is how you can write a HOC for such a case, refer to React docs for more info on the subject.
const withLoader = (loader, Component) =>
class WithLoader extends React.Component {
state = { ready: false, data: null };
async componentDidMount() {
const data = await loader();
this.setState({ ready: true, data });
}
render() {
if (!this.state.ready) return <div>LOADING</div>; // or <ComponentWithAnimation />
return <Component data={this.state.data} />;
}
};
const Test = props => <div>DATA: {props.data}</div>;
const fakeLoader = () =>
new Promise(res => setTimeout(() => res("My data"), 1000));
const TestWithLoader = withLoader(fakeLoader, Test);

Categories

Resources