I'm have a very frustrating time trying to optimize my React-Redux application.
I have a header component that is reloading on every change to the redux store. My header component is a PureComponent
I have installed why-did-you-update, and it tells me:
Header.props: Value did not change. Avoidable re-render!
This is my Component:
export class Header extends PureComponent {
logout() {
// this.props.logout();
}
signup = () => {
this.props.history.push(urls.SIGNUP)
}
render() {
console.log("=============== RELOADING HEADER")
return (
<div>
<HeaderContent
logout={this.logout.bind(this)}
signup={this.signup.bind(this)}
user={this.props.user}/>
</div>
)
}
}
export function mapStateToProps(store) {
// EDITTED: I initially thought this was causing the problem
// but i get the same issue when returning a javascript object
//const u = loginUserFactory(store);
const u ={}
return {
user: u,
}
}
export function mapDispatchToProps(dispatch) {
return {
logout: function l() {
dispatch(authActions.logout())
}
}
}
export function mergeProps(propsFromState,propsFromDispatch,ownProps) {
return {
// logout: function logout() {
// propsFromDispatch.logout()
// },
...propsFromState,
...ownProps
}
}
let HeaderContainer = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{pure: true}
)(Header)
export default withRouter(HeaderContainer);
Header.propTypes = {
history: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
logout: PropTypes.func.isRequired,
}
I have verified that the console.log indicating that the render function is called prints every time the redux store is changed.
If I uncomment the function in merge props whyDidYouUpdate complains that the function caused the re-render.
The re-renders are significantly impacting the performance of my app. I considered writing my own shouldComponentUpdate() function, but read that it is a bad idea to do deep equals in that function for performance reasons.
So what do I do?
EDIT:
This is the code in Login User Factory. Initially I thought this was the problem, but when I remove that code I still get the same issue.
const loginUserFactory = state => {
const u = getLoginUser(state);
const isLoggedIn = !_.isEmpty(u);
const location = getLocation(state);
return {
get id() { return u.id },
get groupNames() { return u.group_names },
get avatarSmall() { return u.avatar_small },
get name() { return u.name },
get email() { return u.email },
// my goal was to localize these methods into one file
// to avoid repeated code and
// to make potential refactoring easier
get location() { return location},
get isHost() {return u.type === "host"},
get isBooker() {return u.type === "booker"},
get isLoggedIn() { return isLoggedIn },
}
}
export default loginUserFactory;
I guess that loginUserFactory() creates a new user object every time it gets called which is every time the store gets updated thus always passing a new user object to your component that is not equal to the previous one.
Also your Header doesn't do anything with the user except passing it further down the tree. You should instead connect the HeaderContent component and only map the properties of the user object to it, that it actually needs, e.g. the name.
In general mapStateToProps() should never have any side effects. It should only filter/sort/calculate the props for the connected component given the state and the own props. In the most trivial cases it does nothing more than returning a subset of properties from the store.
You're using bind in your click handlers. Big no-no! Each rerender will create a completely new instance of the function when you bind inside the handlers. Either bind in a constructor or turn your click handler methods into arrow functions.
handleClick = () => {
}
// or
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}
Also, do not implement any manipulations or algorithms in mapStateToProps or mapDispatchToProps. Those also trigger rerenders. Place that logic somewhere else.
Related
I'm using the following method to control my header from other components. However I'm getting the old "can't perform a react state update on unmounted component" error when changing page
export const store = {
state: {},
setState(value) {
this.state = value;
this.setters.forEach(setter => setter(this.state));
},
setters: []
};
store.setState = store.setState.bind(store);
export function useStore() {
const [ state, set ] = useState(store.state);
if (!store.setters.includes(set)) {
store.setters.push(set);
}
return [ state, store.setState ];
}
My header then uses it to set a class and control if it needs to be black on white or white on black
const Header = () => {
const [type] = useStore();
render( ... do stuff )
};
And my components on page import useStore and then call setType based on a number of factors, certain layouts are one type, some others, some vary depending on API calls so there are a lot of different Components that need to call the function to set the headers state.
const Flexible = (props) => {
const [type, setType] = useStore();
if( type !== 'dark ){ setType('dark') }
... do stuff
};
The header its self is always on page, is before and outside the router and never unmounts.
This all works perfectly fine and sets the headers sate. However when I change page with React Router I get the can't set state error. I can't see why I would get this error. I first thought that the Component might be trying to run again with react router so I moved the code to set the headers state into a useEffect that only runs on initialisation but that didn't help.
You only ever add to the setters, never remove. So when a component unmounts, it will remain in the setters, and the next time some other part of the app tries to set the state, all the setters get called, including the setter for the unmounted component. This then results in the error you're seeing.
You'll need to modify your custom hook to make use of useEffect, so that you can have teardown logic when unmounting. Something like this:
export function useStore() {
const [ state, set ] = useState(store.state);
useEffect(() => {
store.setters.push(set);
return () => {
const i = store.setters.indexOf(set);
if (i > -1) {
store.setters.splice(i, 1);
}
}
}, []);
return [ state, store.setState ];
}
This error is pretty straightforward it means that you are mutating the state (calling setState) in a component that is not mounted.
This mostly happens with promises, you call a promise, then when its resolved you update the state, but if you switch the page before it resolves, when the promise is resolved it still tries to update the state of a component that now its not mounted.
The easy and "ugly" solution, is to use some parameter that you control in componentWillUnmout to check if you still need to update the state or not like this:
var mounted = false;
componentWillMount(){
mounted = true
}
componentWillUnmount(){
mounted = false
}
// then in the promise
// blabla
promise().then(response => {
if(mounted) this.setState();
})
I have seen much more cases related to redirecting users in react applications and every case was just a different approach to the solution. There are some cases, where redirecting has occurred in actions like this`
export const someAction = (values, history) => async dispatch => {
const res = await someAsyncOperation(props);
history.push('/home');
dispatch(someAction);
}
In this example history object (form react-router) is being passed in react component. For me, this approach is not acceptable.
There is also a special Redirect from react-router.
After then I have already searched many articles and couldn't just find anything.
So in your opinion, what's the best practice for redirecting and where to handle such kind of processes ?
In React, you usually achieve redirects in the componentDidUpdate of your components.
In the case of async actions, you will check a flag stored in the Redux store, generally a boolean like isFetching, isCreating, isUpdating, etc…, which will be modified by the actions.
Simple example:
class EditUser extends Component {
compondentDidUpdate(prevProps) {
if (prevProps.isUpdating && !this.props.isUpdating) {
// ↑ this means that the async call is done.
history.push('/users')
}
}
updateUser() {
const modifiedUser = // ...
this.props.updateUser(modifiedUser)
// ↑ will change state.users.isUpdating from false to true during the async call,
// then from true to false once the async call is done.
}
render() {
// ...
<button onClick={this.updateUser}>Update</button>
// ...
}
}
const mapStateToProps = (state, props) => ({
userToEdit: state.users.items.find(user => user.id === props.userId)
isUpdating: state.users.isUpdating,
})
const mapActionsToProps = {
updateUser: usersActions.updateUser,
}
export default connect(mapStateToProps, mapActionsToProps)(EditUser)
The next step is usually to add another flag in your Redux store to track if the async calls are successful or not (e.g. state.users.APIError, in which you can keep the error returned by the API). Then you achieve the redirect only if there are no errors.
We mostly redirect a user due to when user logged in or when sign out. For example here's basic requireAuth HOC component to check if user is logged in or not and redirect him to another place.
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default ChildComponent => {
class ComposedComponent extends Component {
componentDidMount() {
this.shouldNavigateAway();
}
componentDidUpdate() {
this.shouldNavigateAway();
}
shouldNavigateAway() {
if (!this.props.auth) {
this.props.history.push('/');
}
}
render() {
return <ChildComponent {...this.props} />;
}
}
function mapStateToProps(state) {
return { auth: state.auth.authenticated };
}
return connect(mapStateToProps)(ComposedComponent);
};
There are two position to check if user is logged in
When the first time that component mount - in componentDidMount()
When user try to sign in , log in or sign out - in componentDidUpdate()
Also in your code sample, history.push is in an action creator. Action creators belongs to redux side. Keep redux & react separate.
Say I have two redux connected components. The first is a simple todo loading/display container, with the following functions passed to connect(); mapStateToProps reads the todos from the redux state, and mapDispatchToProps is used to request the state to be provided the latest list of todos from the server:
TodoWidgetContainer.js
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
The second redux component is intended to be applied to any component on a page so that component can indicate whether a global "loading" icon is displayed. Since this can be used anywhere, I created a helper function that wraps MapDispatchToProps in a closure and generates an ID for each component, which is used to make sure all components that requested the loader indicate that they don't need it anymore, and the global loader can be hidden.
The functions are basically as follows, with mapStateToProps exposing the loader visibility to the components, and mapDispatchToProps allowing them to request the loader to show or hide.
Loadify.js
function mapStateToProps(state) {
return {
openLoader: loaderSelectors.getLoaderState(state)
};
}
function mapDispatchToProps() {
const uniqId = v4();
return function(dispatch) {
return {
showLoader: () => {
dispatch(loaderActions.showLoader(uniqId));
},
hideLoader: () => {
dispatch(loaderActions.hideLoader(uniqId));
}
};
};
}
export default function Loadify(component) {
return connect(mapStateToProps, mapDispatchToProps())(component);
}
So now, if I have a component that I want to give access to the loader, I can just do something like this:
import Loadify from '...'
class DisplayComponent = new React.Component { ... }
export default Loadify(DisplayComponent);
And it should give it a unique ID, allow it to request the loader to show/hide, and as long as there is one component that is requesting it to show, the loader icon will show. So far, this all appears to be working fine.
My question is, if I would like to apply this to the todos component, so that that component can request/receive its todos while also being allowed to request the loader to show while it is processing, could I just do something like:
TodoWidgetContainer.js
import Loadify from '...'
import TodoWidgetDisplayComponent from '...'
function mapStateToProps(state) {
return {
todos: todoSelectors.getTodos(state)
};
}
function mapDispatchToProps(dispatch) {
return {
refreshTodos: () => dispatch(todoActions.refreshTodos())
};
}
const TodoContainer = connect(mapStateToProps, mapDispatchTo)(TodoWidgetDisplayComponent);
export default Loadify(TodoContainer);
And will redux automatically merge the objects together to make them compatible, assuming there are no duplicate keys? Or will it take only the most recent set of mapStateToProps/mapDispatchTo unless I do some sort of manual merging? Or is there a better way to get this kind of re-usability that I'm not seeing? I'd really rather avoid having to create a custom set of containers for every component we need.
connect will automatically merge together the combination of "props passed to the wrapper component", "props from this component's mapState", and "props from this component's mapDispatch". The default implementation of that logic is simply:
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
So, if you stack multiple levels of connect around each other , the wrapped component will receive all of those props as long as they don't have the same name. If any of those props do have the same name, then only one of them would show up, based on this logic.
Alright, here is what I would do. Create a higher order component (HOC) that adds a new spinner reference to your reducer. The HOC will initialize and destroy references to the spinner in redux by tying into the life cycle methods. The HOC will provide two properties to the base component. The first is isLoading which is a function that takes a boolean parameter; true is on, false is off. The second property is spinnerState that is a readonly boolean of the current state of the spinner.
I created this example without the action creators or reducers, let me know if you need an example of them.
loadify.jsx
/*---------- Vendor Imports ----------*/
import React from 'react';
import { connect } from 'react-redux';
import v4 from 'uuid/v4';
/*---------- Action Creators ----------*/
import {
initNewSpinner,
unloadSpinner,
toggleSpinnerState,
} from '#/wherever/your/actions/are'
const loadify = (Component) => {
class Loadify extends React.Component {
constructor(props) {
super(props);
this.uniqueId = v4();
props.initNewSpinner(this.uniqueId);;
this.isLoading = this.isLoading.bind(this);
}
componentWillMount() {
this.props.unloadSpinner(this.uniqueId);
}
// true is loading, false is not loading
isLoading(isOnBoolean) {
this.props.toggleSpinner(this.uniqueId, isOnBoolean);
}
render() {
// spinners is an object with the uuid as it's key
// the value to the key is weather or not the spinner is on.
const { spinners } = this.props;
const spinnerState = spinners[this.uniqueId];
return (
<Component isLoading={this.isLoading} spinnerState={spinnerState} />
);
}
}
const mapStateTopProps = state => ({
spinners: state.ui.spinners,
});
const mapDispatchToProps = dispatch => ({
initNewSpinner: uuid => dispatch(initNewSpinner(uuid)),
unloadSpinner: uuid => dispatch(unloadSpinner(uuid)),
toggleSpinner: (uuid, isOn) => dispatch(toggleSpinnerState(uuid, isOn))
})
return connect(mapStateTopProps, mapDispatchToProps)(Loadify);
};
export default loadify;
Use Case Example
import loadify from '#/location/loadify';
import Spinner from '#/location/SpinnerComponent';
class Todo extends Component {
componentWillMount() {
this.props.isLoading(true);
asyncCall.then(response => {
// process response
this.props.isLoading(false);
})
}
render() {
const { spinnerState } = this.props;
return (
<div>
<h1>Spinner Testing Component</h1>
{ spinnerState && <Spinner /> }
</div>
);
}
}
// Use whatever state you need
const mapStateToProps = state => ({
whatever: state.whatever.youneed,
});
// use whatever dispatch you need
const mapDispatchToProps = dispatch => ({
doAthing: () => dispatch(doAthing()),
});
// Export enhanced Todo Component
export default loadify(connect(mapStateToProps, mapDispatchToProps)(Todo));
I want to make reusable modules that could be plugged in to any react-redux application. Ideally, my module would have a container component, actions, and reducer at the top level (and then any presentational components below the container). I would want the module to only work off its own slice of the app's state, and ideally to not have to know anything about the rest of the app state (so it's truly modular).
Reducers only work off of part of the state (using combineReducers), so I'm happy there. However, with container components, it seems like mapStateToProps always takes in the full state of the app.
I'd like it if mapStateToProps only took in the same "state slice" that I am handling in my module (like the reducer does). That way my module would truly be modular. Is this possible? I guess I could just pass that slice of the state down to be the props of this component (so I could just use the second argument of mapStateToProps, ownProps), but am not sure if this would have the same effect.
That is actually something of a complicated topic. Because Redux is a single global store, the idea of a completely encapsulated, fully reusable plug-and-play set of logic does become rather difficult. In particular, while the reducer logic can be fairly generic and ignorant of where it lives, the selector functions need to know where in the tree to find that data.
The specific answer to your question is "no, mapState is always given the complete state tree".
I do have links to a number of relevant resources, which may possibly help with your situation:
There's several existing libraries that try to implement "per-component state in Redux". I have a list of them in my Redux addons catalog, in the Component State category.
A group of devs have been discussing and prototyping various approaches to the "reusable logic module in Redux" concept. Their work is at https://github.com/slorber/scalable-frontend-with-elm-or-redux .
Randy Coulman recently posted a three-part blog series related to state encapsulation and modularity in Redux. He didn't come up with definitive answers, but the posts are worth reading: Encapsulating the Redux State Tree, Redux Reducer Asymmetry, and Modular Reducers and Selectors.
Although mapStateToProps (the first function you pass to connect) gets passed the whole store as you said, its job is to map specific parts of the state to the component. So only what is returned from mapStateToProps will be mapped as a prop to your component.
So lets say your state looks like this:
{
account: {
username: "Jane Doe",
email: "janedoe#somemail.com",
password: "12345",
....
},
someOtherStuff: {
foo: 'bar',
foo2: 'bar2'
},
yetMoreStuff: {
usuless: true,
notNeeded: true
}
}
and your component needs everything from account and foo from someOtherStuff then your mapStateToProps would look like this:
const mapStateToProps = ({ account, someOtherStuff }) => ({
account,
foo: { someOtherStuff }
});
export default connect(mapStateToProps)(ComponentName)
then your component will have the prop account and foo mapped from your redux state.
Redux only has a single store as you know, so all it knows to do is pass the entire store to your mapStateToProps function. However using object destructuring, you can specify which properties in the store you want and ignore the rest. Something like 'function mapStateToProps({prop1, prop2})' would only capture those two properties in the store and ignore the rest. Your function is still receiving the entire store, but you're indicating that only these props interest you.
In my example, 'prop1' and 'prop2' would be the names you assigned your reducers during the call to 'combineReducers'.
Ideally the way it works is you get the state and you extract the values from them by use deconstructors. redux works on concept of single state
For example:-
function mapStateToProps(state){
const { auth } = state //just taking a auth as example.
return{
auth
}
}
I'm running into the same problem because, as you said, the current implementation of redux/react-redux allows for splitting up reducers on the state just fine but mapDispatchToProps always passes the whole state tree.
https://stackoverflow.com/a/39757853/444794 is not what I want, because it means we have to duplicate all our selector logic across each react-redux application that uses our module.
My current workaround has been to pass the slice of the state down as a prop instead. This follows a sort of compositional pattern but at the same time removes the cleanliness of accessing the state directly, which I'm disappointed with.
Example:
Generally, you want to do this:
const mapStateToProps = (state) => {
return {
items: mySelector(state)
}
}
const mapDispatchToProps = (dispatch) => {
return {
doStuff: (item) => {
dispatch(doStuff(item))
}
}
}
class ModularComponent extends React.Component {
render() {
return (
<div>
{ this.props.items.map((item) => {
<h1 onclick={ () => this.props.doStuff(item) }>{item.title}</h1>
})}
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ModularComponent)
but since this module is included in an application where the state is now several things (ie. key-values) rather than a list of items, this won't work. My workaround instead looks like:
const mapStateToProps = (_, ownProps) => {
return {
items: mySelector(ownProps.items)
}
}
const mapDispatchToProps = (dispatch) => {
return {
doStuff: (item) => {
dispatch(doStuff(item))
}
}
}
class ModularComponent extends React.Component {
render() {
return (
<div>
{ this.props.items.map((item) => {
<h1 onclick={ () => this.props.doStuff(item) }>{item.title}</h1>
})}
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ModularComponent)
And the application using the Module looks like:
const mapStateToProps = (state) => {
return {
items: state.items
stuffForAnotherModule: state.otherStuff
}
}
class Application extends React.Component {
render() {
return (
<div>
<ModularComponent items={ this.props.items } />
<OtherComponent stuff={ this.props.stuffForAnotherModule } />
</div>
)
}
}
export default connect(mapStateToProps)(Application)
You do have the option of writing a couple of wrapper utils for your modules that will do the work of: 1) Only running mapStateToProps when the module's slice of state changes and 2) only passes in the module's slice into mapStateToProps.
This all assumes your module slices of state are root properties on the app state object (e.g. state.module1, state.module2).
Custom areStatesEqual wrapper function that ensures mapStateToProps will only run if the module's sub-state changes:
function areSubstatesEqual(substateName) {
return function areSubstatesEqual(next, prev) {
return next[substateName] === prev[substateName];
};
}
Then pass it into connect:
connect(mapStateToProps, mapConnectToProps, null, {
areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);
Custom mapStateToProps wrapper that only passes in the module substate:
function mapSubstateToProps(substateName, mapStateToProps) {
var numArgs = mapStateToProps.length;
if (numArgs !== 1) {
return function(state, ownProps) {
return mapStateToProps(state[substateName], ownProps);
};
}
return function(state) {
return mapStateToProps(state[substateName]);
};
}
And you'd use it like so:
function myComponentMapStateToProps(state) {
// Transform state
return props;
}
var mapSubstate = mapSubstateToProps('myModuleSubstateName', myComponentMapStateToProps);
connect(mapSubstate, mapDispatchToState, null, {
areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);
While untested, that last example should only run myComponentMapStateToProps when 'myModuleSubstateName' state changes, and it will only receive the module substate.
One additional enhancement could be to write your own module-based connect function that takes one additional moduleName param:
function moduleConnect(moduleName, mapStateToProps, mapDispatchToProps, mergeProps, options) {
var _mapState = mapSubstateToProps(moduleName, mapStateToProps);
var _options = Object.assign({}, options, {
areStatesEqual: areSubstatesEqual('myModuleSubstateName')
});
return connect(_mapState, mapDispatchToProps, mergeProps, _options);
}
Then each module component would just need to do:
moduleConnect('myModuleName', myMapStateToProps)(MyModuleComponent);
The answer to your question is yes. Both given answers cover different aspects of the same thing. First, Redux creates a single store with multiple reducers. So you'll want to combine them like so:
export default combineReducers({
people: peopleReducer,
departments: departmentsReducer,
auth: authenticationReducer
});
Then, say you have a DepartmentsList component, you may just need to map the departments from the store to your component (and maybe some actions mapped to props as well):
function mapStateToProps(state) {
return { departments: state.departments.departmentsList };
}
export default connect(mapStateToProps, { fetchDepartments: fetchDepartments })(DepartmentsListComponent);
Then inside your component it is basically:
this.props.departments
this.props.fetchDepartments()
I am using this starter kit https://github.com/davezuko/react-redux-starter-kit and am following some tutorials at the same time, but the style of this codebase is slightly more advanced/different than the tutorials I am watching. I am just a little lost with one thing.
HomeView.js - This is just a view that is used in the router, there are higher level components like Root elsewhere I don't think I need to share that, if I do let me know, but it's all in the github link provided above.
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { searchListing } from '../../redux/modules/search'
export class HomeView extends React.Component {
componentDidMount () {
console.log(this.props)
}
render () {
return (
<main onClick={this.props.searchListing}>
<NavBar search={this.props.search} />
<Hero/>
<FilterBar/>
<Listings/>
<Footer/>
</main>
)
}
}
I am using connect() and passing in mapStateToProps to tell the HomeView component about the state. I am also telling it about my searchListing function that is an action which returns a type and payload.
export const searchListing = (value) => {
console.log(value)
return {
type: SEARCH_LISTINGS,
payload: value
}
}
Obviously when I call the method inside the connect() I am passing in an empty object searchListing: () => searchListing({})
const mapStateToProps = (state) => {
return {
search: { city: state.search }
}
}
export default connect((mapStateToProps), { searchListing: () => searchListing({}) })(HomeView)
This is where I am stuck, I am trying to take the pattern from the repo, which they just pass 1, I think anytime that action is created the logic is just add 1 there is no new information passed from the component.
What I am trying to accomplish is input search into a form and from the component pass the users query into the action payload, then the reducer, then update the new state with the query. I hope that is the right idea.
So if in the example the value of 1 is hardcoded and passed into the connect() method, how can I make it so that I am updating value from the component dynamically? Is this even the right thinking?
You almost got it right. Just modify the connect function to pass the action you want to call directly:
const mapStateToProps = (state) => ({
search: { city: state.search }
});
export default connect((mapStateToProps), {
searchListing
})(HomeView);
Then you may use this action with this.props.searchListing(stringToSearch) where stringToSearch is a variable containing the input value.
Notice : You don't seem to currently retrieve the user query. You may need to retrieve it first and then pass it to the searchListing action.
If you need to call a function method, use dispatch.
import { searchListing } from '../../redux/modules/search';
const mapDispatchToProps = (dispatch) => ({
searchListing: () => {
dispatch(searchListing());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeView);
Then, you have made the function a prop, use it with searchListing.