2017. február 7., kedd

React Redux notes

I've started to work on a project where the frontend is using React with Redux and I thought it would make sense to collect all the confusing parts of this ecosystem which made me think about something twice or more. More or less this article's content is available at the official Redux page. Enjoy!

First of all here are the presentational vs container components in comparison:



Presentational Components
Container Components
Purpose
How things look (markup, styles)
How things work (data fetching, state updates)
Aware of Redux
No
Yes
To read data
Read data from props
Subscribe to Redux state
To change data
Invoke callbacks from props
Dispatch Redux actions
Are written
By hand
Usually generated by React Redux

This is not rocket science, but still, it is a really good separation between components. In one sentence: containers are Redux aware while components are not, they are just getting everything via props (both parts of the state and callback functions which will call some dispatches on the Redux store).

Redux

Here is a silly picture of Redux, but actually it represents well how the state changes are managed.

An action creator is basically a helper for actions where you can pass an argument and it will return an action, nothing fancy.
When are action creators used? Here is a great article: https://daveceddia.com/redux-action-creators/. To sum it up, whenever you need to pass some dynamic values like username or e-mail.
combineReducers:
when there are lots of reducers (a function which handles a call of an action and creates the new state regarding to that action) it is tideous to write a rootReducer like this:
function rootReducer(state = {}, action) {
return {
reducer1: reducer1(state.treeNode1, action),
reducer2: reducer2(state.treeNode2, action),

}};

Very important! Each of these reducers are managing its own part of the global state. The state parameter is different for every reducer, and corresponds to the part of the state it manages.
Actually combineReducers is simplifying this with the following syntax:
import { combineReducers } from ‘redux’;
const app = combineReducers({
reducer1,
reducer2,

});
export default app;
All combineReducers() does is generate a function that calls your reducers with the slices of state selected according to their keys, and combining their results into a single object again.
Store
  • Holds application state;
  • Allows access to state via getState();
  • Allows state to be updated via dispatch(action);
  • Registers listeners via subscribe(listener);
  • Handles unregistering of listeners via the function returned by subscribe(listener).
data flow (how redux handles actions)
  • You call store.dispatch(action).
  • The Redux store calls the reducer function you gave it.
  • The root reducer may combine the output of multiple reducers into a single state tree.
  • The Redux store saves the complete state tree returned by the root reducer.
    - Every listener registered with store.subscribe(listener) will now be invoked; listeners may call store.getState() to get the current state.

React-Redux

  • connect function:
    Basically what you could do is to subscribe to state changes at container components. But this is tideous and react-redux’s connect function does performance improvements where it is calling shouldCompentUpdate in an optimal way.
    • mapStateToProps
      With this function you are able to get a subtree of the Redux store as a prop for a component.
    • mapDispatchToProps
      This function enables you to bind functions which dispatches actions on certain events which were fired by the component.
  • Provider
    It’s a component.
    All container components need access to the Redux store so they can subscribe to it. One option would be to pass it as a prop to every container component. However it gets tedious, as you have to wire store even through presentational components just because they happen to render a container deep in the component tree.
    Provider makes the store available to all container components in the application without passing it explicitly
  • What's the difference between React's state vs props?
    state is a private model while props are sort of public
    “A component may choose to pass its state down as props to its child components.”
    So basically you can’t reach state from the outside, but you can tell parts of the state via props to a component “below” in the component tree)

React component lifecycle diagram

A very simple (dumb) implementation of Redux

Sometimes I get really confused how the Redux environment is getting around. These times I get back to this very simplificated implementation of Redux (which is actually a pretty goo starting point).

Async Redux

Well, that's a bit more complicated of a topic. There are a few options if you would like to go with HTTP calls e.g.
E.g. there are great libraries like redux-thunk, redux-promise, redux-saga and many-many more.
Let's talk about redux-thunk first.
The action creator can return a function instead of an action object.
When an action creator returns a function, that function will get executed by the Redux Thunk middleware. This function doesn't need to be pure; it is thus allowed to have side effects, including executing asynchronous API calls. The function can also dispatch actions—like those synchronous actions we defined earlier.
Why do we need redux-thunk? (LINK) We could easily do the following (calling dispatch in the callback) this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' }) setTimeout(() => { this.props.dispatch({ type: 'HIDE_NOTIFICATION' }) }, 5000) OR with action creators // actions.js export function showNotification(text) { return { type: 'SHOW_NOTIFICATION', text } } export function hideNotification() { return { type: 'HIDE_NOTIFICATION' } } // component.js import { showNotification, hideNotification } from '../actions' this.props.dispatch(showNotification('You just logged in.')) setTimeout(() => { this.props.dispatch(hideNotification()) }, 5000) OR with the connect() function: this.props.showNotification('You just logged in.') setTimeout(() => { this.props.hideNotification() }, 5000) The problem with this approach is you ca have race conditions, meaning in the above example if two components are waiting for the noticfication request to end, one will dispatch HIDE_NOTIFICATION which is going to hide the second notification erroneously. What we could do is to extract the action creator like the following: // actions.js function showNotification(id, text) { return { type: 'SHOW_NOTIFICATION', id, text } } function hideNotification(id) { return { type: 'HIDE_NOTIFICATION', id } } let nextNotificationId = 0 export function showNotificationWithTimeout(dispatch, text) { // Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION // for the notification that is not currently visible. // Alternatively, we could store the interval ID and call // clearInterval(), but we’d still want to do it in a single place. const id = nextNotificationId++ dispatch(showNotification(id, text)) setTimeout(() => { dispatch(hideNotification(id)) }, 5000) } Now separate components will work with the async call: // component.js showNotificationWithTimeout(this.props.dispatch, 'You just logged in.') // otherComponent.js showNotificationWithTimeout(this.props.dispatch, 'You just logged out.') showNotificationWithTimeout need dispatch as an argument, because that function is not part of the component, but it still needs to make changes on the store. (BAD APPROACH: If we had a singleton store exported from some module, then the function does not need the dispatch as an argument. But this si not a good approach since it forces the store to be singleton. (which makes testing harder, becuase mocking is difficult, because it is referencing the same store object)) Now comes the thunk middleware in play. showNotificationWithTimeout is not returning an action, so it’s not an action creator, but it’s sort of because of that purpose. This was the motivation for finding a way to “legitimize” this pattern of providing dispatch to a helper function, and help Redux “see” such asynchronous action creators as a special case of normal action creators rather than totally different functions. With this approach we can declare showNotificationWithTimeout function as regular Redux action creator! // actions.js function showNotification(id, text) { return { type: 'SHOW_NOTIFICATION', id, text } } function hideNotification(id) { return { type: 'HIDE_NOTIFICATION', id } } let nextNotificationId = 0 export function showNotificationWithTimeout(text) { return function (dispatch) { const id = nextNotificationId++ dispatch(showNotification(id, text)) setTimeout(() => { dispatch(hideNotification(id)) }, 5000) } } Important note: showNotificationWithTimeout doesn’t accept dispatch now as an argument, instead returns a function that accepts dispatch as the first argument. Neat! In the component it will look like this: // component.js showNotificationWithTimeout('You just logged in.')(this.props.dispatch) But that looks weird! Instead what we can do is this: // component.js this.props.dispatch(showNotificationWithTimeout('You just logged in.')) Also worth mentioning that the second argument of the thunk (the returned function) is the getState method, which gets us access to the store. Also worth mentioning that not only redux-thunk is there to do async dispatches, but redux-saga (with generators and promises, like async-await) or redux loop.

Summary of async redux flow

Without middleware, Redux store only supports synchronous data flow. This is what you get by default with createStore(). You may enhance createStore() with applyMiddleware(). It is not required, but it lets you express asynchronous actions in a convenient way. Asynchronous middleware like redux-thunk or redux-promise wraps the store's dispatch() method and allows you to dispatch something other than actions, for example, functions or Promises. Any middleware you use can then interpret anything you dispatch, and in turn, can pass actions to the next middleware in the chain. For example, a Promise middleware can intercept Promises and dispatch a pair of begin/end actions asynchronously in response to each Promise. When the last middleware in the chain dispatches an action, it has to be a plain object. This is when the synchronous Redux data flow takes place.

Nincsenek megjegyzések:

Megjegyzés küldése