In my redux containers, I have to dispatch pretty complex actions taking a lot of properties from the store. I cannot find the right pattern to tackle the problem without crushing the performances.
Let's take the example of a container that would only contain a Send button to send a message:
(For such a small example, any of the following approach would work well, I am just trying to illustrate a problem I encounter in way bigger containers.)
function mapStateToProps(state) {
return {
user: selectors.selectedUser(state),
title: selectors.title(state),
message: selectors.message(state),
};
}
function dispatchProps(state) {
return {
onClickSend: function(user, title, message) {
actions.sendMessage({user, title, message});
}
};
}
If I do that, my simple Send button will have to be aware of a lot of useless properties:
SendBtn.propTypes = {
user: PropTypes.string,
title: PropTypes.string,
message: PropTypes.string,
onClickSend: PropTypes.func,
}
And also to call onClickSend with all those props:
onClickSend(user, title, message)
That's way too much knowledge for the Send button. The button should only know it has to call onClickSend
when we click on it, this component is a simple button. It shouldn't know about any other props. Also, in a more complex case, it could be not just 2 props (title and message) that are needed but 5 or 10.
Another problem is performances, everytime I am going to modify the message or the title (=== on every keystroke), the send button is going to be re-rendered.
The current approach of my app is currently using is relying on mergeProps:
function mapStateToProps(state) {
return {
user: selectors.selectedUser(state),
title: selectors.title(state),
message: selectors.message(state),
};
}
function mergeProps(stateProps, dispatchProps, ownProps) {
const {user, title, message} = stateProps;
const newProps = {
onClickSend: actions.sendMessage.bind(null, {
user,
title,
message
});
};
return R.mergeAll([stateProps, dispatchProps, ownProps, newProps]);
}
I find this approach way better because the Send button only has to know that it must fire its unique property: onClickSend when clicked. No need to worry about user, title or message.
However, this approach has a huge drawback: the reference of onClickSend is going to change everytime the store changes, which would lead to really poor performances on larger real-life cases.
A solution to the performance issue could be to use redux-thunk and access the state directly in the action.
// Action file
var sendMessageAction = createAction("SEND_MESSAGE", function(dispatch, getState) {
// executed by redux thunk
const state = getState();
const args = {
user: selectors.selectedUser(state),
title: selectors.title(state),
message: selectors.message(state),
}
dispatch(sendMessage(args))
})
// Container file
function mapDispatchToProps(dispatch) {
onClickSend: dispatch(sendMessageAction())
}
But I don't like this approach because:
I have worked for a while on a big redux app now and it is by far the biggest pain I have with Redux. Surprisingly, I don't find too much information about how to solve it so I'm wondering if I'm missing something elementary.
What is your approach of that problem?
mergeProps is a function that handles combining the props passed directly to a compoment, and from the two previous mapping functions. it takes three arguments: ownProps – which contains all of the props passed into the component outside of the connect function.
In fact, React Redux in particular is heavily optimized to cut down on unnecessary re-renders, and React-Redux v5 shows noticeable improvements over earlier versions. Redux may not be as efficient out of the box when compared to other libraries.
The mapStateToProps and mapDispatchToProps deals with your Redux store's state and dispatch , respectively. state and dispatch will be supplied to your mapStateToProps or mapDispatchToProps functions as the first argument.
How does Redux connect() work? The connect() function provided by React Redux can take up to four arguments, all of which are optional. Calling the connect() function returns a higher order component, which can be used to wrap any React component.
You're missing a fourth approach (similar to your mergeProps
option): have the parent component pass a bound-up function or callback closure that captures those values, like:
// using bind()
<SendButton onClick={sendMessage.bind(null, user, title, message)}
// or, as an inline arrow function
SendButton onClick={() => this.sendMessage(user, title, message)}
I suppose in theory a fifth approach might be to connect the parent of the SendButton
, have it pass a very simple click handler to the button, and let the parent worry about the arguments from its own props (ie, keep the button itself completely presentational, and put the logic in the parent of the button). Actually, I guess that's pretty much the suggestion that @samanime had.
Which approach you use is really up to you. I personally would probably lean towards that last one, just so that it doesn't re-create a function every time there's a re-render. (In particular, I would avoid the mergeProps
approach, as that will re-create a function every time the store updates, which will be even more often than the component re-renders.)
I actually addressed your questions about whether accessing state in thunks breaks "uni-directional data flow" in my blog post Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability. As a summary, I don't think it actually does break that unidirectional flow, and my opinion is that accessing state in thunks is a completely valid approach.
As a side note, I generally recommend that people use the object shorthand argument for binding methods with connect
, instead of writing an actual mapDispatchToProps
function:
const mapDispatch = {
onClickSend : actions.sendMessage
};
export default connect(mapState, mapDispatch)(MyComponent);
To me, there's almost never a reason to actually write out a separate mapDispatch
function (which I also talk about in my post Idiomatic Redux: Why Use Action Creators?).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With