React Context children re-rendering to initial state when updating context state - reactjs

I'm having an issue where, whenever I update my context state within a hook (useEffect, useCallback, etc.), any other state updates that I make don't actually go through because the state for that component resets to initial state.
I have the following component which provides the message context, its provider, and exposes a hook to access the message state variable's value and setter:
const MessageContext = React.createContext<MessageContextProps>({
message: {} as IMessage,
setMessage: () => {},
});
export function MessageProvider(props: MessageProviderProps): JSX.Element {
const [message, setMessage] = useState<IMessage>({} as IMessage);
return <MessageContext.Provider value={{ message, setMessage }}>{props.children}</MessageContext.Provider>;
}
export function useMessage(): MessageContextProps {
return React.useContext(MessageContext);
}
My app is wrapped in the provider:
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<MessageProvider>
<App />
</MessageProvider>
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
);
Whenever I try to update the formData in my component (setFormData(newFormData)) in the same render cycle as I update the message state (setMessage()) from MessageContext, only the message state updates. The rest of my state updates don't re-render (specifically, the formData.changedValue value in the final paragraph tag), I think because it's getting reset to initial state.
export default function App(): JSX.Element {
const [formData, setFormData] = useState(INITIAL_STATE);
const { message, setMessage } = useMessage();
const [updateSuccessful, setUpdateSuccessful] = useState(false);
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
makeSomeAPICall(formData);
setUpdateSuccessful(true);
},
[formData]
);
useEffect(() => {
if (updateSuccessful) {
setFormData((current) => {
return {
...current,
changedValue: current.myField,
};
});
setMessage({
displayText: `Change Successful`,
type: 'success',
});
}
}, [updateSuccessful]);
return (
<>
<form onSubmit={handleSubmit}>
<div>
<select id='changeMe' value={formData.myField} onChange={() => setFormData(formData.myField)}>
<option value='Y'>Y</option>
<option value='N'>N</option>
</select>
<button type='submit'>Submit</button>
</form>
<p>Changed Value: {formData.changedValue}</p>
</>
);
}

Related

React Native open only a specific block [duplicate]

I have a question, if I can use useState generic in React Hooks, just like I can do this in React Components while managing multiple states?
state = {
input1: "",
input2: "",
input3: ""
// .. more states
};
handleChange = (event) => {
const { name, value } = event.target;
this.setState({
[name]: value,
});
};
Yes, with hooks you can manage complex state (without 3rd party library) in three ways, where the main reasoning is managing state ids and their corresponding elements.
Manage a single object with multiple states (notice that an array is an object).
Use useReducer if (1) is too complex.
Use multiple useState for every key-value pair (consider the readability and maintenance of it).
Check out this:
// Ids-values pairs.
const complexStateInitial = {
input1: "",
input2: "",
input3: ""
// .. more states
};
function reducer(state, action) {
return { ...state, [action.type]: action.value };
}
export default function App() {
const [fromUseState, setState] = useState(complexStateInitial);
// handle generic state from useState
const onChangeUseState = (e) => {
const { name, value } = e.target;
setState((prevState) => ({ ...prevState, [name]: value }));
};
const [fromReducer, dispatch] = useReducer(reducer, complexStateInitial);
// handle generic state from useReducer
const onChangeUseReducer = (e) => {
const { name, value } = e.target;
dispatch({ type: name, value });
};
return (
<>
<h3>useState</h3>
<div>
{Object.entries(fromUseState).map(([key, value]) => (
<input
key={key}
name={key}
value={value}
onChange={onChangeUseState}
/>
))}
<pre>{JSON.stringify(fromUseState, null, 2)}</pre>
</div>
<h3>useReducer</h3>
<div>
{Object.entries(fromReducer).map(([key, value]) => (
<input
name={key}
key={key}
value={value}
onChange={onChangeUseReducer}
/>
))}
<pre>{JSON.stringify(fromReducer, null, 2)}</pre>
</div>
</>
);
}
Notes
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Refer to React Docs.
The correct way to do what you're trying to do is to create your own hook that uses useState internally.
Here is an example:
// This is your generic reusable hook.
const useHandleChange = (initial) => {
const [value, setValue] = React.useState(initial);
const handleChange = React.useCallback(
(event) => setValue(event.target.value), // This is the meaty part.
[]
);
return [value, handleChange];
}
const App = () => {
// Here we use the hook 3 times to show it's reusable.
const [value1, handle1] = useHandleChange('one');
const [value2, handle2] = useHandleChange('two');
const [value3, handle3] = useHandleChange('three');
return <div>
<div>
<input onChange={handle1} value={value1} />
<input onChange={handle2} value={value2} />
<input onChange={handle3} value={value3} />
</div>
<h2>States:</h2>
<ul>
<li>{value1}</li>
<li>{value2}</li>
<li>{value3}</li>
</ul>
</div>
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Note the use of React.useCallback to stop your hook from returning a new handler function on every render. (We don't need to specify setValue as a dependency because React guarantees that it will never change)
I didn't actually test this, but it should work.
See https://reactjs.org/docs/hooks-reference.html#usestate for more info.
import React, {useState} from 'react';
const MyComponent = () => {
const [name, setName] = useState('Default value for name');
return (<div><button onClick={()=>setName('John Doe')}}>Set Name</button></div>);
};
export default MyComponent;

Render data in React Hooks

Hello I'm try to Render data in a component to another component, which are siblings to one another. with useState Hook(rff) based on component code (rcf)
index.js -> is entry point, that calls only one component App, as we have no route
App.js -> is the parent component, which has two child, Certification and Panel
Certification.js -> takes input
Panel -> renders data from certification
I know i have problem with handleFromCert (this is my handle change function)
here my code -rff
https://codesandbox.io/s/zen-paper-gil-xcyj3?file=/src/
here the code that based on rcf and work fine
https://codesandbox.io/s/elegant-shtern-362ki?file=/src/
I corrected the code and now it works!
handleFromCert in App.js should receive name and value;
value2 in the Panel component in App.js is passed with an error;
handleFromCert in Certifications.js setValue changes incorrectly.
Certifications.js
import React, { useState } from "react";
const Certifications = (props) => {
const [value, setValue] = useState({
value1: "",
value2: ""
});
const handleFromCert = ({ target: { value, name } }) => {
setValue(prevState => ({ ...prevState, [name]: value }));
props.handleFromCert(name, value);
};
return (
<div>
<input name="value1" onChange={handleFromCert} />
<input name="value2" onChange={handleFromCert} />
<div>
Inside certificate
<div>{value.value1}</div>
<div>{value.value2}</div>
Certificate ends
</div>
</div>
);
};
export default Certifications;
App.js
import React, { useState } from "react";
import Certifications from "./Certifications";
import Panel from "./Panel";
const App = () => {
const [value, setValue] = useState({
value1: "",
value2: ""
});
const handleFromCert = (name, value) =>
setValue((prevState) => ({ ...prevState, [name]: value }));
return (
<div>
{value.value1}
{value.value2}
<Certifications handleFromCert={handleFromCert} />
<Panel value1={value.value1} value2={value.value2} />
</div>
);
};
export default App;
The problem is that you're not passing the event as the argument, you're passing the value and your function is expecting the event. In your Certification component change this:
const handleFromCert = (e) => {
setValue({
[e.target.name]: e.target.value
});
props.handleFromCert((e.target.name, e.target.value));
};
To this:
const handleFromCert = (e) => {
setValue({
[e.target.name]: e.target.value
});
props.handleFromCert(e);
};
Your function handleFromCert is expecting the event, and you're just passing the value which is a string and cannot be destructured like the event.
Here's the sandbox working for me:
https://codesandbox.io/s/zen-paper-gil-forked-r01fh

React hooks useEffect calls mutiple times when redux store other data changed

my code like this:
Info component:
import {
getAttachData,
} from '#src/actions/creators/account'
const Info: React.FC = () => {
const info = useSelector<any, Account>(state => state.getIn(['account', 'info']).toJS())
const list = useSelector<any, Data[]>(state => state.getIn(['account', 'list']).toJS())
const attach = useSelector<any, AttachData[]>(state => state.getIn(['account', 'attach']).toJS())
...
const handleChange = ({ select }) => {
dispatch(getAttachData({v: select}))
}
const Template = (params) => {
return (
<div>
<BaseSelect onChange={(val) => handleChange(val)} list={list} />}
</div>
)
}
return (
...
<Template data={info} />
{attach.map((child, cidx) => (<Template data={child} />))}
)
}
export default Info
BaseSelect component:
const BaseSelect: React.FC<Props> = props => {
const [selectId, setSelectId] = useState('')
const { list } = props
useEffect(() => {
if (!isEmpty(list)) {
...
}
console.log('init')
}, [])
const handleChange = (value) => {
setSelectId(value)
props.onChange({
select: value,
})
}
return (
<Select
data={list}
value={selectId}
onChange={handleChange}
/>
)
}
export default BaseSelect
when excute handleChange event in BaseSelect component, the props.onChange function will call handleChange event in info component, and dispatch http request getAttachData which will change attach data in redux store, but useEffect in BaseSelect component will also excute and in console will print 'init' two times.
console:
It's because your Template component re-creates every time when redux store is changing.
Just move Template component outside the Info component.

Why is React component rerendering when props has not changed?

I have built an app on ReactJS 16.8.5 and React-Redux 3.7.2. When the app loads the app mounts, initial store is set and database subscriptions are set up against a Firebase Realtime Database. The app contains a sidebar, header and content section. By profiling the app using React Developer Tools I can see that the Sidebar is being rendered several times - triggering rerender of child components. I have implemented React.memo to avoid rerendring when props change.
From what I can see the props does not change, but the Sidebar still rerenders, which confuses me.
app.js
//Imports etc...
const jsx = (
<React.StrictMode>
<Provider store={store}>
<AppRouter />
</Provider>
</React.StrictMode>
)
let hasRendered = false
const renderApp = () => {
if (!hasRendered) { //make sure app only renders one time
ReactDOM.render(jsx, document.getElementById('app'))
hasRendered = true
}
}
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// Set initial store and db subscriptions
renderApp()
}
})
AppRouter.js
//Imports etc...
const AppRouter = ({}) => {
//...
return (
<React.Fragment>
//uses Router instead of BrowserRouter to use our own history and not the built in one
<Router history={history}>
<div className="myApp">
<Route path="">
<Sidebar />
</Route>
//More routes here...
</div>
</Router>
</React.Fragment>
)
}
//...
export default connect(mapStateToProps, mapDispatchToProps)(AppRouter)
Sidebar.js
//Imports etc...
export const Sidebar = (props) => {
const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
if (id !== 'Sidebar') { return }
console.log('Profile', phase, actualDuration)
}
return (
<Profiler id="Sidebar" onRender={onRender}>
<React.Fragment>
{/* Contents of Sidebar */}
</React.Fragment>
</Profiler>
}
const mapStateToProps = (state) => {
console.log('Sidebar mapStateToProps')
return {
//...
}
}
const areEqual = (prevProps, nextProps) => {
const areStatesEqual = _.isEqual(prevProps, nextProps)
console.log('Profile Sidebar isEqual', areStatesEqual)
return areStatesEqual
}
export default React.memo(connect(mapStateToProps, mapDispatchToProps)(Sidebar),areEqual)
Console output
Sidebar mapStateToProps 2
Profile Sidebar mount 225
Sidebar mapStateToProps
Profile Sidebar isEqual true
Sidebar mapStateToProps
Profile Sidebar update 123
Sidebar mapStateToProps 2
Profile Sidebar update 21
Sidebar mapStateToProps
Profile Sidebar update 126
Sidebar mapStateToProps
Profile Sidebar update 166
Sidebar mapStateToProps
Profile Sidebar update 99
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Profile Sidebar update 110
Sidebar mapStateToProps
Sidebar mapStateToProps
Sidebar mapStateToProps
Profile Sidebar update 4
Why is the Sidebar rerendering eight times when the props has not changed? One rerender would be expected?
Kind regards /K
As commented; when mapStateToProps returns a new object it will re render the connected component even if no relevant values change.
This is because {} !== {}, an object with same props and values does not equal another object with same props and values because React compares object reference and not the values of the object. That is why you can't change state by mutating it. Mutating changes the values in the object but not the reference to the object.
Your mapStateToProps has to return a new reference at the 2nd level for it to re render with the same values, so {val:1} won't re render but {something:{val:1}} will.
The code below shows how not memoizing the result of mapStateToProps can cause re renders:
const { Provider, connect, useDispatch } = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const { useRef, useEffect, memo } = React;
const state = { val: 1 };
//returning a new state every action but no values
// have been changed
const reducer = () => ({ ...state });
const store = createStore(
reducer,
{ ...state },
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
const Component = (props) => {
const rendered = useRef(0);
rendered.current++;
return (
<div>
<div>rendered:{rendered.current} times</div>
props:<pre>{JSON.stringify(props)}</pre>
</div>
);
};
const selectVal = (state) => state.val;
const selectMapStateToProps = createSelector(
selectVal,
//will only re create this object when val changes
(val) => console.log('val changed') || { mem: { val } }
);
const memoizedMapStateToProps = selectMapStateToProps;
const mapStateToProps = ({ val }) =>
({ nonMem: { val } }); //re creates props.nonMem every time
const MemoizedConnected = connect(memoizedMapStateToProps)(
Component
);
//this mapStateToProps will create a props of {val:1}
// pure components (returned by connect) will compare each property
// of the prop object and not the props as a whole. Since props.val
// never changed between renders it won't re render
const OneLevelConnect = connect(({ val }) => ({ val }))(
Component
);
const Connected = connect(mapStateToProps)(Component);
const Pure = memo(function Pure() {
//props never change so this will only be rendered once
console.log('props never change so wont re render Pure');
return (
<div>
<Connected />
<MemoizedConnected />
<OneLevelConnect />
</div>
);
});
const App = () => {
const dispatch = useDispatch();
useEffect(
//dispatch an action every second, this will create a new
// state ref but state.val never changes
() => {
setInterval(() => dispatch({ type: 88 }), 1000);
},
[dispatch] //dispatch never changes but linting tools don't know that
);
return <Pure />;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
The mapStateToProps function can also be optimised more by passing a function that returns a function. This way you can create a memoized selector when the component mounts. This can be used in list items (see code below).
const { useRef, useEffect } = React;
const {
Provider,
useDispatch,
useSelector,
connect,
} = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const state = {
data: [
{
id: 1,
firstName: 'Ben',
lastName: 'Token',
},
{
id: 2,
firstName: 'Susan',
lastName: 'Smith',
},
],
};
//returning a new state every action but no values
// have been changed
const reducer = () => ({ ...state });
const store = createStore(
reducer,
{ ...state },
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
//selectors
const selectData = (state) => state.data;
const selectPerson = createSelector(
selectData,
(_, id) => id, //pass second argument to select person by id
(people, _id) => people.find(({ id }) => id === _id)
);
//function that will create props for person component
// from person out of state
const asPersonProps = (person) => ({
person: {
fullName: person.firstName + ' ' + person.lastName,
},
});
//in ConnectedPerson all components share this selector
const selectPersonProps = createSelector(
(state, { id }) => selectPerson(state, id),
asPersonProps
);
//in OptimizedConnectedPerson each component has it's own
// selector
const createSelectPersonProps = () =>
createSelector(
(state, { id }) => selectPerson(state, id),
asPersonProps
);
const Person = (props) => {
const rendered = useRef(0);
rendered.current++;
return (
<li>
<div>rendered:{rendered.current} times</div>
props:<pre>{JSON.stringify(props)}</pre>
</li>
);
};
//optimized mapStateToProps
const mapPersonStateToProps = createSelectPersonProps;
const OptimizedConnectedPerson = connect(
mapPersonStateToProps
)(Person);
const ConnectedPerson = connect(selectPersonProps)(Person);
const App = () => {
const dispatch = useDispatch();
const people = useSelector(selectData);
const rendered = useRef(0);
rendered.current++;
useEffect(
//dispatch an action every second, this will create a new
// state ref but state.val never changes
() => {
setInterval(() => dispatch({ type: 88 }), 1000);
},
[dispatch] //dispatch never changes but linting tools don't know that
);
return (
<div>
<h2>app rendered {rendered.current} times</h2>
<h3>Connected person (will re render)</h3>
<ul>
{people.map(({ id }) => (
<ConnectedPerson key={id} id={id} />
))}
</ul>
<h3>
Optimized Connected person (will not re render)
</h3>
<ul>
{people.map(({ id }) => (
<OptimizedConnectedPerson key={id} id={id} />
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

React Hooks: Trying to access state before unmount returns initial state

In general i'm trying to save global state updates when my component unmounts because react-apollo gives me a hard time with unnecessary refetches.
I'm adding all the deleted comment ids to deletedCommentsQueue and when the Comments component unmounts i want to updated my global state but when the component about to unmount deletedCommentsQueue changes to an empty array even though we can see all the comment ids before we try to do our update.
I've made a simple SandBox for you guys.
And this is my code for anyone who's interested
import React, { useState, useEffect, useContext, createContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserContext = createContext();
const comments = [
{ _id: 1, body: "first" },
{ _id: 2, body: "second" },
{ _id: 3, body: "third" }
];
const Comments = ({ commentIds }) => {
const [deletedCommentsQueue, setDeletedCommentsQueue] = useState([]);
const addToQueue = commentId => {
setDeletedCommentsQueue([...deletedCommentsQueue, commentId]);
};
const { loggedUser, setLoggedUser } = useContext(UserContext);
useEffect(
() => () => {
console.log("cleaning");
console.log("deletedCommentsQueue", deletedCommentsQueue);
const updatedComments = loggedUser.comments.filter(
commentId => !deletedCommentsQueue.includes(commentId)
);
console.log(updatedComments);
setLoggedUser({
...loggedUser,
comments: updatedComments,
likes: {
...loggedUser.likes,
comments: loggedUser.likes.comments.filter(
commentId => !deletedCommentsQueue.includes(commentId)
)
}
});
},
[]
);
return (
<div>
{deletedCommentsQueue.length > 0 && (
<h1>Comment ids for deletion {deletedCommentsQueue.join(" ")}</h1>
)}
{commentIds.map(commentId => (
<Comment
deleted={deletedCommentsQueue.includes(commentId)}
key={commentId}
comment={comments.find(c => c._id === commentId)}
deleteCommentFromCache={() => addToQueue(commentId)}
/>
))}
</div>
);
};
const Comment = ({ comment, deleted, deleteCommentFromCache }) => (
<div>
{deleted && <h2>Deleted</h2>}
<p>{comment.body}</p>
<button disabled={deleted} onClick={deleteCommentFromCache}>
Delete
</button>
</div>
);
const App = () => {
const [loggedUser, setLoggedUser] = useState({
username: "asafaviv",
comments: [1, 2, 3],
likes: {
comments: [1, 2]
}
});
const [mounted, setMounted] = useState(true);
return (
<div className="App">
<UserContext.Provider value={{ loggedUser, setLoggedUser }}>
{mounted && <Comments commentIds={loggedUser.comments} />}
</UserContext.Provider>
<br />
<button onClick={() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"}
</button>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I guess it's because of the empty dependency ([]) of the effect that you declare. That way, the effect is run once when the component is mounted.
Therefore, I guess in the context of its closure, deletedCommentQueues has the value of when the component was first mounted => [].
I tried your codesandbox and if you remove that [] (which means the effect is called on each update), you get the correct value when you unmount the component but...the function is called on each update which does not solve your caching problem.
IMHO, I would suggest that you set your state (const [deletedCommentsQueue, setDeletedCommentsQueue] = useState([]);) in the parent component and save wherever you want the data as soon as the mounted value turns to false instead of watching from inside the component.

Resources