Why is React component rerendering when props has not changed? - reactjs

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>

Related

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

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>
</>
);
}

Using redux connect with React hooks

This isn't a question about react-redux hooks like useSelector or useDispatch. I'm curious to know how old school react-redux connect() with a functional component and when it's necessary to use React hooks like useEffect in this example.
Suppose I have this functional component that renders a "Hello world" in green if someReduxData is present, otherwise it render it in red.
const RandomComponent = ({ someReduxData }) => {
const style = {
color: someReduxData ? "green" : "red";
};
return (
<div style={style}>Hello world</div>
);
}
const mapStateToProps = (state) => {
return {
someReduxData: state.someReduxData;
};
};
export default connect(mapStateToProps)(RandomComponent);
Let's say when the component first mounts to the DOM, someReduxData is null. Then it changes state so it's not null anymore. Will this force a re-render of RandomComponent so it renders in green? If not, then I assume I will need to listen for changes on someReduxData with useEffect()?
It will force a re-render of RandomComponent. connect works the same regardless of class vs function component.
Here's an example using a function component: There's a setTimeout of 2 seconds before an action dispatches that turns the App div green.
There's also an example of that same component using a hook instead of connect.
The main difference between connect and hooks are that connect essentially acts as React.memo for the component. This can be toggled off with a boolean flag, but you likely will never have to do that: https://react-redux.js.org/api/connect#pure-boolean
const initialstate = {
someReduxData: null,
};
const reducer = (state = initialstate, action) => {
switch (action.type) {
case 'action':
return {
someReduxData: action.data,
};
default:
return initialstate;
}
};
const actionCreator = (data) => {
return {
type: 'action',
data,
};
};
const store = Redux.createStore(reducer);
const App = ({ someReduxData }) => {
return (
<div className={someReduxData}>
Some div -- Will turn green in 2 seconds
</div>
);
};
const mapStateToProps = (state) => {
return {
someReduxData: state.someReduxData,
};
};
const WrappedApp = ReactRedux.connect(mapStateToProps)(App);
const AppWithHooks = () => {
const someReduxData = ReactRedux.useSelector(state=>state.someReduxData);
return (
<div className={someReduxData}>
Some div with a hook -- Will turn green in 2 seconds
</div>
);
};
ReactDOM.render(
<ReactRedux.Provider store={store}>
<WrappedApp />
<AppWithHooks />
</ReactRedux.Provider>,
document.querySelector('#root')
);
setTimeout(() => store.dispatch(actionCreator('green')), 2000);
.green {
background-color: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>
<div id="root" />

Getting react passed props & redux state at same time in component

Im trying to get access to redux state but im also need props that passed from routing.
The example is: i need that props
const DefaultLayout = props => {
return (
<div>
</div>
)
}
because
<Route
path="/"
name="Home"
render={props => <DefaultLayout {...props} />}
/>
when i add redux state like: {auth: {user}} to access user data, its not working.
const DefaultLayout = (props , {auth: {user}}) => {
return (
<div>
</div>
)
}
...
DefaultLayout.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(DefaultLayout);
if i delete that props i will getting pathname error, any explanation and help? new to react.
I think you need to access the props this way :
const DefaultLayout = ({auth, location, history, ...otherProps}) => {
//example : console.log(otherProps.match);
return (
<div>
{auth.user}
</div>
)
}
Props passed by the Route component are merged with the props added by redux.
const DefaultLayout = (props , {auth: {user}}) => {
Components just get passed a single variable: props. If you want to access auth.user, it's found at props.auth.user. It's put there by mapStateToProps in cooperation with connect
const DefaultLayout = (props) => {
const { location, history, match, auth } = props;
return (
<div>
// something with the variables
</div>
)
}
const mapStateToProps = state => ({
auth: state.auth
})
export default connect(mapStateToProps)(DefaultLayout);

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.

How does redux state changes but component doesn't

I'm writing a independent modal using React and Redux. I pass from my environment variable if modal is visible and initial position and the rest of the state in redux store.
I've tried using react lifecycle methods to force update my app but nothing seems to work.
This is how I connect my App with store:
render() {
const {
media, initPosition, isMobile, title, isVisible, onClose
} = this.props;
const photos = media.filter(
item => typeof item.video === 'undefined'
);
const videos = media.filter(
item => typeof item.video !== 'undefined'
);
const initState = {
media: {
items: media,
filteredItems: {
all: media,
photos,
videos
},
filter: 'all',
initPosition,
currentPosition: initPosition
},
gallery: {
isMobile,
title
}
};
const store = createStore(
reducer,
initState,
composeEnhancers(applyMiddleware(...middleware))
);
return (
<Provider store={store}>
<App onClose={e => onClose(e)} isVisible={isVisible} />
</Provider>
);
I call my modal like this:
<Gallery
media={videos.concat(photos)}
isMobile={isMobile}
isVisible={show}
onClose={() => this.setState({ show: false })}
initPosition={position}
changePosition={position => this.setState({ position })}
title="Maximus"/>
And this is how I connect it to the state:
function mapStateToProps(state) {
const { media, gallery } = state;
const {
filteredItems, filter, currentPosition, initPosition
} = media;
const { isMobile, title } = gallery;
return {
filteredMedia: filteredItems,
filter,
currentPosition,
initPosition,
isMobile,
title
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
changeMediaProp
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(
GalleryApp
);
After isVisible is changed nothing seem to work with redux store. It is changing, but the app isn't updating.
When I toggle modal (change isVisible prop), redux state keeps changing, but my app isn't rerendering.
So to sum it up. I change isVisible and initPosition from surrounded application( these props are not stored in store), and when I changed them my component can't react to changes from reducer store.
I was passing multiple stores to my application. I fixed it by saving store in constructor and not creating it multiple times.
let newStore = store;
if (!newStore) {
newStore = createStore(
reducer,
initState,
composeEnhancers(applyMiddleware(...middleware))
);
this.setState({ store: newStore });
}
return (
<Provider store={newStore}>
<App onClose={e => onClose(e)} isVisible={isVisible} />
</Provider>
);
Does anyone have any better solution?
Your code doesn't have enough information to know. Are you using connect and react-redux. Here's a good intro if you need some help.
https://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/
An example component would look like this:
import { connect } from 'react-redux'
const TodoItem = ({ todo, destroyTodo }) => {
return (
<div>
{todo.text}
<span onClick={destroyTodo}> x </span>
</div>
)
}
const mapStateToProps = state => {
return {
todo: state.todos[0]
}
}
const mapDispatchToProps = dispatch => {
return {
destroyTodo: () =>
dispatch({
type: 'DESTROY_TODO'
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TodoItem)

Resources