React setState / Flux - Does it always trigger a re-render? - reactjs

I've been playing around with React Native lately and I reached a point where I became interested in managing my state more properly, as a start achieving a shared state between all the components.
The answer of course is Flux. Before moving forward with some more advanced solutions (e.g. Redux, Alt, MobX) I thought I should start with understanding the raw structure itself, with the help of one small tool, that is the Flux dispatcher.
import React, { Component } from 'react';
import { AppRegistry, Text, View } from 'react-native';
import EventEmitter from 'EventEmitter';
import { Dispatcher } from 'flux';
class Store extends EventEmitter {
list = [];
actions = {
add: title => dispatcher.dispatch({ type: 'add', payload: { title } })
};
handle = ({ type, payload }) => {
switch(type) {
case 'add': this.add(payload.title); break;
}
};
add(title) {
this.list.push(title);
this.emit('change');
}
}
const store = new Store(), dispatcher = new Dispatcher();
dispatcher.register(store.handle);
class App extends Component {
state = { list: store.list };
componentWillMount() {
this.listener = store.addListener('change', () => this.setState({ list: store.list }));
}
componentDidMount() {
setInterval(() => store.actions.add(new Date().getTime()), 1000);
}
componentWillUnmount() { this.listener.remove(); }
render() {
return (
<View style={{ marginTop: 20 }}>
<Text>{JSON.stringify(this.state.list)}</Text>
</View>
);
}
}
AppRegistry.registerComponent('straightforwardFlux', () => App);
Notice in the view layer, we have {JSON.stringify(this.state.data)}, naturally when the store is updated the view will be re-rendered since it is linked to the state.
When changing to {JSON.stringify(store.data)} the view is also re-rendered! this shouldn't happen because the view should only update when there is a change in the state that affect the view directly, in this case there is no state rendered in the view whatsoever. Am I missing something here? why would we encounter this behaviour?
This leads to another question, does render() get called every time there is a state change? even if it doesn't affect the way the view layer looks? I've looked into this and I got two different answers, one says yes and that componentShouldUpdate() returns true by default, meaning that some changes need to be made here (if so, how?), and the other one was simply no, it doesn't update with each setState().
Overall, is this implementation correct?

Per the documentation...
setState() will always trigger a re-render unless conditional rendering logic is implemented in shouldComponentUpdate(). If mutable objects are being used and the logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
tl;dr; React isn't analyzing your view to see explicitly what state it is depending on, that is up to you to optimize with shouldComponentUpdate().
shouldComponentUpdate(nextProps, nextState) {
// check if your view's attributes changes
let check1 = nextState.foo != this.state.foo
let check2 = nextState.bar != this.state.bar
// if return is true, the component will rerender
return check1 || check2
}

Related

Where is the best place to make calculations outside the render method in React?

I have a render method in my container component like this:
render() {
const { validationErrors } = this.state
const { errorsText, errorsFields} = validationErrors.reduce(
(acc, error) => {
acc.errorsText.push(error.text)
acc.errorsFields[error.field.toLowerCase()] = true
return acc
},
{
errorsText: [],
errorsFields: {},
},
)
return (
<MyViewComponent
errorsText={errorsText}
errorsFields={errorsFields}
/>
)
}
As you can see every render there are some computations happens (returned array and object with the new values), then I pass it into my child component as a props. I have a feeling that this is a wrong pattern. We should keep render function 'pure'. Isn't it? The question is: Where is the best place for making such computations outside the render?
If this were a functional component (which I highly recommend you use in the future, by the way), you'd be able to use the 'hook' useEffect to recalculate errorsText and errorsField whenever this.state.validationErrors changes, and only when it changes.
For your Class Component, however, I assume at some point you set this.state.validationErrors. What you should do is create a method that runs your reducer and stores errorsText and errorsField to state, then place a call to this method after each point you set this.state.validationErrors. Then, remove the logic in the render method and replace errorsText and errorsField with this.state.errorsText and this.state.errorsField respectively.
Doing this will ensure you only ever run your reducer when necessary (i.e. when this.state.validationErrors changes).
Your component would end up looking something like this:
class MyComponent extends Component {
...
someCallback() {
const validationErrors = someFunctionThatReturnsErrors();
// We do the logic here, because we know that validationErrors
// could have changed value
const { errorsText, errorsFields } = validationErrors.reduce(
(acc, error) => {
acc.errorsText.push(error.text);
acc.errorsFields[error.field.toLowerCase()] = true;
return acc;
}, {
errorsText: [],
errorsFields: {},
},
);
// Put everything in the state
this.setState({
validationErrors, // you may not even need to set this if it's not used elsewhere`
errorsText,
errorsFields
});
}
...
render() {
const {
errorsText,
errorsFields
} = this.state;
return (
<MyViewComponent
errorsText={errorsText}
errorsFields={errorsFields}
/>
);
}
}
It is pure, as it has no side effects.
As long as this does not create performance issues I see no problem with this. If it does create performance issues, you should look into memoizing the reduce. If you were using hooks you could use the built-in React.useMemo for this. While using class version you could look into something like https://www.npmjs.com/package/memoize-one

ReactJS: Deep nested state

Let's say I have this React component:
class SomeComponent extends Component {
constructor(props) {
super(props);
this.state = {
topObject: {
childObject1: {
grandChildObj1: {
attr1: this.props.val1,
attr2: this.props.val2
}
},
childProp: 1
},
topProp: 2
};
}
render() {
return (
<div>
<span>{this.state.topObject.childObject.grandChildObject.attr1}
</span>
</div>
// ...
)
}
changeDeepNestedStateValue(val) {
// need code here to change the state
// set topObj.childObject.grandChildObject.attr1
// to the 'val' argument
}
}
What code would I need inside the function 'changeDeepNestedStateValue' so that it changes the state immutably so that React detects the change and re-renders?
Are deep-nested state values a bad practice or anti-pattern? If so, is there an optimal structure to a state, a flat one maybe?
Deep nesting is not necessarily an anti-pattern but just makes your code harder to maintain and reason about. In order to trigger a re-render and update component state, all you have to do in the changeDeepNestedStateValue function is call this.setState({ topObject: {...} }) with whatever new state you want to update. Optionally, this.setState also takes a function that exposes the previous state of the component as seen below.
this.setState((prevState) => {
if (prevState.topProp === val) {
doSomething();
}
});
I'd recommend having a look at immer
https://github.com/mweststrate/immer
It provides a super easy way to work with nested objects in terms of immutablilty
But yes, flatter state with out of the box react state management is better practice

React subscriptions which depend on state

We are currently refactoring to use higher-order components. For the most part this is making everything much simpler.
We have HOCs for fetching data and listening to stores. For example, we have connectStores, which takes a list of stores to subscribe to and a function to fetch the data (to pass as extra props):
connectStores(FooComponent, [FooStore], function (props) {
return {
foo: FooStore.get(props.id),
};
});
However, there are a few places where the process of fetching the data from the store depends upon the state. For example, we have a SelectFooPopup the presents the user with a list of items to select from. But there is also a search box to filter the list, so at the moment the component listens directly to the store and then fetches the data itself like this:
componentDidMount() {
var self = this;
this.listenTo(FooStore, 'change', function () {
self.forceUpdate();
});
}
render() {
var items = FooStore.search(this.state.searchText);
// render...
}
(this.listenTo is a mixin which we're trying to replace with HOCs so we can use ES6 classes)
I can think of a few options, but I don't like any of them:
Option 1: Remove listenTo and cleanup the listener manually
componentDidMount() {
var self = this;
this.listener = function () {
self.forceUpdate();
};
FooStore.on('change', this.listener);
}
componentWillUnmount() {
if (this.listener) {
FooStore.removeListener('change', this.listener);
}
}
render() {
var items = FooStore.search(this.state.searchText);
// render...
}
I really hate having to do this manually. We did this before we had the listenTo mixin and it's far too easy to get wrong.
This also doesn't help when the subscription has to fetch the data from the server directly rather than using a pre-filled store.
Option 2: Use connectStores but don't return any extra data
class SelectFooPopup extends React.Component {
render() {
var items = FooStore.search(this.state.searchText);
}
}
connectStores(SelectFooPopup, [FooStore], function (props) {
// Just to forceUpdate
return {};
});
This just feels wrong to me. This is asking for trouble when we start optimising for pure components and suddenly the child component doesn't re-render anymore.
Option 3: Use connectStores to fetch all the data and then filter it in render
class SelectFooPopup extends React.Component {
render() {
var items = filterSearch(this.props.items, this.state.searchText);
}
}
connectStores(SelectFooPopup, [FooStore], function (props) {
return {
items: FooStore.getAllItems(),
};
});
But now I have to have a completely separate filterSearch function. Shouldn't this be a method on the store?
Also, it doesn't make much difference in this example, but I have other components with a similar issue where
they are fetching data from the server rather than subscribing to a pre-filled store. In these cases the
data set is far too large to send it all and filter later, so the searchText must be available when fetching the data.
Option 4: Create a parent component to hold the state
Sometimes this is the right solution. But it doesn't feel right here. The searchText is part of the state of this component. It belongs in the same place that renders the search box.
Moving it to a separate component is confusing and artificial.
Option 5: Use a "parentState" HOC
function parentState(Component, getInitialState) {
class ParentStateContainer extends React.Component {
constructor(props) {
super();
this.setParentState = this.setParentState.bind(this);
if (getInitialState) {
this.state = getInitialState(props);
} else {
this.state = {};
}
}
setParentState(newState) {
this.setState(newState);
}
render() {
return <Component {...this.props} {...this.state} setParentState={ this.setParentState } />;
}
}
return ParentStateContainer;
}
// Usage:
parentState(SelectFooPopup, function (props) {
return {
searchText: '',
};
});
// In handleSearchText:
this.props.setParentState({ searchText: newValue });
This also feels really wrong and I should probably throw this away.
Conclusion
In React we have 2 levels: props and state.
It seems to me that there are actually 4 levels to think about:
props
data that depends on props only
state
data that depends on props and state
render
We can implement layer 2 using HOCs. But how can we implement layer 4?

Actions (playing sound) based on Redux store changes

I'm building an app that contains a store with an "offers" section of the state tree (ImmutableJS List Object). I need to take some action (play a browser sound) whenever an item is added to this list. Items can be added to this list via several different types of Redux actions.
I am trying to figure out the best way to react to the changes to a particular part of the store. I could do it in each action/reducer method, but then I would have it all over the place. I'd rather have one central place to handle the logic.
What's the best way to handle this? Should I create a generic store subscriber and has it's own logic for keeping track of the list values?
In this case your best bet is a store listener. Either a plain listener function or a redux connected React component.
Assuming a simple function to make noise:
function playSound () {
const audio = new Audio('audio_file.mp3')
audio.play()
}
You can create a store observer and listen for changes:
function createSoundObserver (store) {
let prevState = store.getState()
return store.subscribe(() => {
const nextState = store.getState()
if (prevState.messages.length < nextState.messages.length) {
playSound()
}
prevState = nextState
})
}
You can achieve the same with a React component:
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
class Notifier extends Component {
static propTypes = {
messages: PropTypes.array.isRequired
}
componentDidUpdate (prevProps) {
if (this.props.messages.length > prevProps.messages.length) {
playSound()
}
}
render () { return null }
}
export default connect((state, props) => {
const {messages} = state
return {messages}
}, {})(Notifier)
As long as a Notifier is present amongst the rendered tree, it will check for changes and play the sound accordingly. The advantage of this approach is that you don't have to take extra care of unsubscribing the event if you want to stay quiet, and it seamlessly works server-side rendering.

Re-render React component when prop changes

I'm trying to separate a presentational component from a container component. I have a SitesTable and a SitesTableContainer. The container is responsible for triggering redux actions to fetch the appropriate sites based on the current user.
The problem is the current user is fetched asynchronously, after the container component gets rendered initially. This means that the container component doesn't know that it needs to re-execute the code in its componentDidMount function which would update the data to send to the SitesTable. I think I need to re-render the container component when one of its props(user) changes. How do I do this correctly?
class SitesTableContainer extends React.Component {
static get propTypes() {
return {
sites: React.PropTypes.object,
user: React.PropTypes.object,
isManager: React.PropTypes.boolean
}
}
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
render() {
return <SitesTable sites={this.props.sites}/>
}
}
function mapStateToProps(state) {
const user = userUtils.getCurrentUser(state)
return {
sites: state.get('sites'),
user,
isManager: userUtils.isManager(user)
}
}
export default connect(mapStateToProps)(SitesTableContainer);
You have to add a condition in your componentDidUpdate method.
The example is using fast-deep-equal to compare the objects.
import equal from 'fast-deep-equal'
...
constructor(){
this.updateUser = this.updateUser.bind(this);
}
componentDidMount() {
this.updateUser();
}
componentDidUpdate(prevProps) {
if(!equal(this.props.user, prevProps.user)) // Check if it's a new user, you can also use some unique property, like the ID (this.props.user.id !== prevProps.user.id)
{
this.updateUser();
}
}
updateUser() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Using Hooks (React 16.8.0+)
import React, { useEffect } from 'react';
const SitesTableContainer = ({
user,
isManager,
dispatch,
sites,
}) => {
useEffect(() => {
if(isManager) {
dispatch(actions.fetchAllSites())
} else {
const currentUserId = user.get('id')
dispatch(actions.fetchUsersSites(currentUserId))
}
}, [user]);
return (
return <SitesTable sites={sites}/>
)
}
If the prop you are comparing is an object or an array, you should use useDeepCompareEffect instead of useEffect.
componentWillReceiveProps() is going to be deprecated in the future due to bugs and inconsistencies. An alternative solution for re-rendering a component on props change is to use componentDidUpdate() and shouldComponentUpdate().
componentDidUpdate() is called whenever the component updates AND if shouldComponentUpdate() returns true (If shouldComponentUpdate() is not defined it returns true by default).
shouldComponentUpdate(nextProps){
return nextProps.changedProp !== this.state.changedProp;
}
componentDidUpdate(props){
// Desired operations: ex setting state
}
This same behavior can be accomplished using only the componentDidUpdate() method by including the conditional statement inside of it.
componentDidUpdate(prevProps){
if(prevProps.changedProp !== this.props.changedProp){
this.setState({
changedProp: this.props.changedProp
});
}
}
If one attempts to set the state without a conditional or without defining shouldComponentUpdate() the component will infinitely re-render
You could use KEY unique key (combination of the data) that changes with props, and that component will be rerendered with updated props.
componentWillReceiveProps(nextProps) { // your code here}
I think that is the event you need. componentWillReceiveProps triggers whenever your component receive something through props. From there you can have your checking then do whatever you want to do.
I would recommend having a look at this answer of mine, and see if it is relevant to what you are doing. If I understand your real problem, it's that your just not using your async action correctly and updating the redux "store", which will automatically update your component with it's new props.
This section of your code:
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Should not be triggering in a component, it should be handled after executing your first request.
Have a look at this example from redux-thunk:
function makeASandwichWithSecretSauce(forPerson) {
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
You don't necessarily have to use redux-thunk, but it will help you reason about scenarios like this and write code to match.
A friendly method to use is the following, once prop updates it will automatically rerender component:
render {
let textWhenComponentUpdate = this.props.text
return (
<View>
<Text>{textWhenComponentUpdate}</Text>
</View>
)
}
You could use the getDerivedStateFromProps() lifecyle method in the component that you want to be re-rendered, to set it's state based on an incoming change to the props passed to the component. Updating the state will cause a re-render. It works like this:
static getDerivedStateFromProps(nextProps, prevState) {
return { myStateProperty: nextProps.myProp};
}
This will set the value for myStateProperty in the component state to the value of myProp, and the component will re-render.
Make sure you understand potential implications of using this approach. In particular, you need to avoid overwriting the state of your component unintentionally because the props were updated in the parent component unexpectedly. You can perform checking logic if required by comparing the existing state (represented by prevState), to any incoming props value(s).
Only use an updated prop to update the state in cases where the value from props is the source of truth for the state value. If that's the case, there may also be a simpler way to achieve what you need. See - You Probably Don't Need Derived State – React Blog.

Resources