Why do we need middleware for async flow in Redux?

前端 未结 11 2216
忘了有多久
忘了有多久 2020-11-22 04:17

According to the docs, \"Without middleware, Redux store only supports synchronous data flow\". I don\'t understand why this is the case. Why can\'t the container component

相关标签:
11条回答
  • 2020-11-22 04:47

    When working in an enterprise project, there are many requirements available in middle-ware such as (saga) not available in simple asynchronous flow, below are some:

    • Running request in parallel
    • Pulling future actions without the need to wait
    • Non-blocking calls Race effect, example pickup first
    • response to initiate the process Sequencing your tasks (first in first call)
    • Composing
    • Task cancellation Dynamically forking the task.
    • Support Concurrency Running Saga outside the redux middleware.
    • Using channels

    The list is long just review the advanced section in saga documentation

    0 讨论(0)
  • 2020-11-22 04:48

    OK, let's start to see how middleware working first, that quite answer the question, this is the source code applyMiddleWare function in Redux:

    function applyMiddleware() {
      for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
        middlewares[_key] = arguments[_key];
      }
    
      return function (createStore) {
        return function (reducer, preloadedState, enhancer) {
          var store = createStore(reducer, preloadedState, enhancer);
          var _dispatch = store.dispatch;
          var chain = [];
    
          var middlewareAPI = {
            getState: store.getState,
            dispatch: function dispatch(action) {
              return _dispatch(action);
            }
          };
          chain = middlewares.map(function (middleware) {
            return middleware(middlewareAPI);
          });
          _dispatch = compose.apply(undefined, chain)(store.dispatch);
    
          return _extends({}, store, {
            dispatch: _dispatch
          });
        };
      };
    }
    

    Look at this part, see how our dispatch become a function.

      ...
      getState: store.getState,
      dispatch: function dispatch(action) {
      return _dispatch(action);
    }
    
    • Note that each middleware will be given the dispatch and getState functions as named arguments.

    OK, this is how Redux-thunk as one of the most used middlewares for Redux introduce itself:

    Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.

    So as you see, it will return a function instead an action, means you can wait and call it anytime you want as it's a function...

    So what the heck is thunk? That's how it's introduced in Wikipedia:

    In computer programming, a thunk is a subroutine used to inject an additional calculation into another subroutine. Thunks are primarily used to delay a calculation until it is needed, or to insert operations at the beginning or end of the other subroutine. They have a variety of other applications to compiler code generation and in modular programming.

    The term originated as a jocular derivative of "think".

    A thunk is a function that wraps an expression to delay its evaluation.

    //calculation of 1 + 2 is immediate 
    //x === 3 
    let x = 1 + 2;
    
    //calculation of 1 + 2 is delayed 
    //foo can be called later to perform the calculation 
    //foo is a thunk! 
    let foo = () => 1 + 2;
    

    So see how easy the concept is and how it can help you manage your async actions...

    That's something you can live without it, but remember in programming there are always better, neater and proper ways to do things...

    0 讨论(0)
  • 2020-11-22 04:50

    To use Redux-saga is the best middleware in React-redux implementation.

    Ex: store.js

      import createSagaMiddleware from 'redux-saga';
      import { createStore, applyMiddleware } from 'redux';
      import allReducer from '../reducer/allReducer';
      import rootSaga from '../saga';
    
      const sagaMiddleware = createSagaMiddleware();
      const store = createStore(
         allReducer,
         applyMiddleware(sagaMiddleware)
       )
    
       sagaMiddleware.run(rootSaga);
    
     export default store;
    

    And then saga.js

    import {takeLatest,delay} from 'redux-saga';
    import {call, put, take, select} from 'redux-saga/effects';
    import { push } from 'react-router-redux';
    import data from './data.json';
    
    export function* updateLesson(){
       try{
           yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
           yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
       }
       catch(e){
          console.log("error",e)
         }
      }
    
    export function* updateDetail(action) {
      try{
           //To write store update details
       }  
        catch(e){
           console.log("error",e)
        } 
     }
    
    export default function* rootSaga(){
        yield [
            updateLesson()
           ]
        }
    

    And then action.js

     export default function updateFruit(props,fruit) {
        return (
           {
             type:"UPDATE_DETAIL",
             payload:fruit,
             props:props
           }
         )
      }
    

    And then reducer.js

    import {combineReducers} from 'redux';
    
    const fetchInitialData = (state=[],action) => {
        switch(action.type){
          case "INITIAL_DATA":
              return ({type:action.type, payload:action.payload});
              break;
          }
         return state;
      }
     const updateDetailsData = (state=[],action) => {
        switch(action.type){
          case "INITIAL_DATA":
              return ({type:action.type, payload:action.payload});
              break;
          }
         return state;
      }
    const allReducers =combineReducers({
       data:fetchInitialData,
       updateDetailsData
     })
    export default allReducers; 
    

    And then main.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './app/components/App.jsx';
    import {Provider} from 'react-redux';
    import store from './app/store';
    import createRoutes from './app/routes';
    
    const initialState = {};
    const store = configureStore(initialState, browserHistory);
    
    ReactDOM.render(
           <Provider store={store}>
              <App />  /*is your Component*/
           </Provider>, 
    document.getElementById('app'));
    

    try this.. is working

    0 讨论(0)
  • 2020-11-22 04:53

    Redux can't return a function instead of an action. It's just a fact. That's why people use Thunk. Read these 14 lines of code to see how it allows the async cycle to work with some added function layering:

    function createThunkMiddleware(extraArgument) {
      return ({ dispatch, getState }) => (next) => (action) => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    
    export default thunk;
    

    https://github.com/reduxjs/redux-thunk

    0 讨论(0)
  • 2020-11-22 04:56

    Abramov's goal - and everyone's ideally - is simply to encapsulate complexity (and async calls) in the place where it's most appropriate.

    Where's the best place to do that in the standard Redux dataflow? How about:

    • Reducers? No way. They should be pure functions with no side-effects. Updating the store is serious, complicated business. Don't contaminate it.
    • Dumb View Components? Definitely No. They have one concern: presentation and user-interaction, and should be as simple as possible.
    • Container Components? Possible, but sub-optimal. It makes sense in that the container is a place where we encapsulate some view related complexity and interact with the store, but:
      • Containers do need to be more complex than dumb components, but it's still a single responsibility: providing bindings between view and state/store. Your async logic is a whole separate concern from that.
      • By placing it in a container, you'd be locking your async logic into a single context, for a single view/route. Bad idea. Ideally it's all reusable, and totally decoupled.
    • Some other Service Module? Bad idea: you'd need to inject access to the store, which is a maintainability/testability nightmare. Better to go with the grain of Redux and access the store only using the APIs/models provided.
    • Actions and the Middlewares that interpret them? Why not?! For starters, it's the only major option we have left. :-) More logically, the action system is decoupled execution logic that you can use from anywhere. It's got access to the store and can dispatch more actions. It has a single responsibility which is to organize the flow of control and data around the application, and most async fits right into that.
      • What about the Action Creators? Why not just do async in there, instead of in the actions themselves, and in Middleware?
        • First and most important, the creators don't have access to the store, as middleware does. That means you can't dispatch new contingent actions, can't read from the store to compose your async, etc.
        • So, keep complexity in a place that's complex of necessity, and keep everything else simple. The creators can then be simple, relatively pure functions that are easy to test.
    0 讨论(0)
  • 2020-11-22 04:59

    You don't.

    But... you should use redux-saga :)

    Dan Abramov's answer is right about redux-thunk but I will talk a bit more about redux-saga that is quite similar but more powerful.

    Imperative VS declarative

    • DOM: jQuery is imperative / React is declarative
    • Monads: IO is imperative / Free is declarative
    • Redux effects: redux-thunk is imperative / redux-saga is declarative

    When you have a thunk in your hands, like an IO monad or a promise, you can't easily know what it will do once you execute. The only way to test a thunk is to execute it, and mock the dispatcher (or the whole outside world if it interacts with more stuff...).

    If you are using mocks, then you are not doing functional programming.

    Seen through the lens of side-effects, mocks are a flag that your code is impure, and in the functional programmer's eye, proof that something is wrong. Instead of downloading a library to help us check the iceberg is intact, we should be sailing around it. A hardcore TDD/Java guy once asked me how you do mocking in Clojure. The answer is, we usually don't. We usually see it as a sign we need to refactor our code.

    Source

    The sagas (as they got implemented in redux-saga) are declarative and like the Free monad or React components, they are much easier to test without any mock.

    See also this article:

    in modern FP, we shouldn’t write programs — we should write descriptions of programs, which we can then introspect, transform, and interpret at will.

    (Actually, Redux-saga is like a hybrid: the flow is imperative but the effects are declarative)

    Confusion: actions/events/commands...

    There is a lot of confusion in the frontend world on how some backend concepts like CQRS / EventSourcing and Flux / Redux may be related, mostly because in Flux we use the term "action" which can sometimes represent both imperative code (LOAD_USER) and events (USER_LOADED). I believe that like event-sourcing, you should only dispatch events.

    Using sagas in practice

    Imagine an app with a link to a user profile. The idiomatic way to handle this with each middleware would be:

    redux-thunk

    <div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
    
    function loadUserProfile(userId) {
      return dispatch => fetch(`http://data.com/${userId}`)
        .then(res => res.json())
        .then(
          data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
          err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
        );
    }
    

    redux-saga

    <div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
    
    
    function* loadUserProfileOnNameClick() {
      yield* takeLatest("USER_NAME_CLICKED", fetchUser);
    }
    
    function* fetchUser(action) {
      try {
        const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
        yield put({ type: 'USER_PROFILE_LOADED', userProfile })
      } 
      catch(err) {
        yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
      }
    }
    

    This saga translates to:

    every time a username gets clicked, fetch the user profile and then dispatch an event with the loaded profile.

    As you can see, there are some advantages of redux-saga.

    The usage of takeLatest permits to express that you are only interested to get the data of the last username clicked (handle concurrency problems in case the user click very fast on a lot of usernames). This kind of stuff is hard with thunks. You could have used takeEvery if you don't want this behavior.

    You keep action creators pure. Note it's still useful to keep actionCreators (in sagas put and components dispatch), as it might help you to add action validation (assertions/flow/typescript) in the future.

    Your code becomes much more testable as the effects are declarative

    You don't need anymore to trigger rpc-like calls like actions.loadUser(). Your UI just needs to dispatch what HAS HAPPENED. We only fire events (always in the past tense!) and not actions anymore. This means that you can create decoupled "ducks" or Bounded Contexts and that the saga can act as the coupling point between these modular components.

    This means that your views are more easy to manage because they don't need anymore to contain that translation layer between what has happened and what should happen as an effect

    For example imagine an infinite scroll view. CONTAINER_SCROLLED can lead to NEXT_PAGE_LOADED, but is it really the responsibility of the scrollable container to decide whether or not we should load another page? Then he has to be aware of more complicated stuff like whether or not the last page was loaded successfully or if there is already a page that tries to load, or if there is no more items left to load? I don't think so: for maximum reusability the scrollable container should just describe that it has been scrolled. The loading of a page is a "business effect" of that scroll

    Some might argue that generators can inherently hide state outside of redux store with local variables, but if you start to orchestrate complex things inside thunks by starting timers etc you would have the same problem anyway. And there's a select effect that now permits to get some state from your Redux store.

    Sagas can be time-traveled and also enables complex flow logging and dev-tools that are currently being worked on. Here is some simple async flow logging that is already implemented:

    Decoupling

    Sagas are not only replacing redux thunks. They come from backend / distributed systems / event-sourcing.

    It is a very common misconception that sagas are just here to replace your redux thunks with better testability. Actually this is just an implementation detail of redux-saga. Using declarative effects is better than thunks for testability, but the saga pattern can be implemented on top of imperative or declarative code.

    In the first place, the saga is a piece of software that permits to coordinate long running transactions (eventual consistency), and transactions across different bounded contexts (domain driven design jargon).

    To simplify this for frontend world, imagine there is widget1 and widget2. When some button on widget1 is clicked, then it should have an effect on widget2. Instead of coupling the 2 widgets together (ie widget1 dispatch an action that targets widget2), widget1 only dispatch that its button was clicked. Then the saga listen for this button click and then update widget2 by dispaching a new event that widget2 is aware of.

    This adds a level of indirection that is unnecessary for simple apps, but make it more easy to scale complex applications. You can now publish widget1 and widget2 to different npm repositories so that they never have to know about each others, without having them to share a global registry of actions. The 2 widgets are now bounded contexts that can live separately. They do not need each others to be consistent and can be reused in other apps as well. The saga is the coupling point between the two widgets that coordinate them in a meaningful way for your business.

    Some nice articles on how to structure your Redux app, on which you can use Redux-saga for decoupling reasons:

    • http://jaysoo.ca/2016/02/28/organizing-redux-application/
    • http://marmelab.com/blog/2015/12/17/react-directory-structure.html
    • https://github.com/slorber/scalable-frontend-with-elm-or-redux

    A concrete usecase: notification system

    I want my components to be able to trigger the display of in-app notifications. But I don't want my components to be highly coupled to the notification system that has its own business rules (max 3 notifications displayed at the same time, notification queueing, 4 seconds display-time etc...).

    I don't want my JSX components to decide when a notification will show/hide. I just give it the ability to request a notification, and leave the complex rules inside the saga. This kind of stuff is quite hard to implement with thunks or promises.

    I've described here how this can be done with saga

    Why is it called a Saga?

    The term saga comes from the backend world. I initially introduced Yassine (the author of Redux-saga) to that term in a long discussion.

    Initially, that term was introduced with a paper, the saga pattern was supposed to be used to handle eventual consistency in distributed transactions, but its usage has been extended to a broader definition by backend developers so that it now also covers the "process manager" pattern (somehow the original saga pattern is a specialized form of process manager).

    Today, the term "saga" is confusing as it can describe 2 different things. As it is used in redux-saga, it does not describe a way to handle distributed transactions but rather a way to coordinate actions in your app. redux-saga could also have been called redux-process-manager.

    See also:

    • Interview of Yassine about Redux-saga history
    • Kella Byte: Claryfing the Saga pattern
    • Microsoft CQRS Journey: A Saga on Sagas
    • Medium response of Yassine

    Alternatives

    If you don't like the idea of using generators but you are interested by the saga pattern and its decoupling properties, you can also achieve the same with redux-observable which uses the name epic to describe the exact same pattern, but with RxJS. If you're already familiar with Rx, you'll feel right at home.

    const loadUserProfileOnNameClickEpic = action$ =>
      action$.ofType('USER_NAME_CLICKED')
        .switchMap(action =>
          Observable.ajax(`http://data.com/${action.payload.userId}`)
            .map(userProfile => ({
              type: 'USER_PROFILE_LOADED',
              userProfile
            }))
            .catch(err => Observable.of({
              type: 'USER_PROFILE_LOAD_FAILED',
              err
            }))
        );
    

    Some redux-saga useful resources

    • Redux-saga vs Redux-thunk with async/await
    • Managing processes in Redux Saga
    • From actionsCreators to Sagas
    • Snake game implemented with Redux-saga

    2017 advises

    • Don't overuse Redux-saga just for the sake of using it. Testable API calls only are not worth it.
    • Don't remove thunks from your project for most simple cases.
    • Don't hesitate to dispatch thunks in yield put(someActionThunk) if it makes sense.

    If you are frightened of using Redux-saga (or Redux-observable) but just need the decoupling pattern, check redux-dispatch-subscribe: it permits to listen to dispatches and trigger new dispatches in listener.

    const unsubscribe = store.addDispatchListener(action => {
      if (action.type === 'ping') {
        store.dispatch({ type: 'pong' });
      }
    });
    
    0 讨论(0)
提交回复
热议问题