Skip to content

liujian10/react-hooks-realize-redux

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

为什么要用Redux?

一般而言,如果随着时间的推移,数据处于合理的变动之中、需要一个单一的数据源、在 React 顶层组件 state 中维护所有内容的办法已经无法满足需求,这个时候就需要使用 Redux 了

这是官方关于什么时候需要用到Redux的描述,分析下

  1. 数据处于合理的变动之中 > 数据复杂度越来越高
  2. 需要一个单一数据源 > 统一状态/数据管理
  3. 在 React 顶层组件 state 中维护所有内容的办法已经无法满足需求 > React无法满足需求

简单来说,Redux 提供了统一的状态管理,让复杂的前端变得更加健壮和易维护,让组件之间数据共享变得更有效率,简单列下Redux的优点:

  1. 统一的状态管理,让组件间数据共享更高效
  2. reducer纯函数和action机制,使状态具有可预测性,易于测试
  3. 可以把state从组件中解耦出来,使组件更轻量,代码易读性更好
  4. 各种middleware,方便拓展

React Hooks 实现 Redux

Redux很好,现在我们每个React项目几乎都会用到Redux。但有时候它也会显得很臃肿,actionreducerconnnectmapStateToPropsmapDispatchToProps写起来会显得很繁琐,有没有更优雅的方案呢?

之前看到一篇有意思的文章 使用 React Hooks 代替 Redux ,好像React Hooks是个不错的替代方案,不多说,直接上代码

Redux

  • 组织action
// actions/actions.js
export const increment = count => ({
  type: 'CHANGE_COUNT',
  payload: count + 1
})

export const decrement = count => ({
  type: 'CHANGE_COUNT',
  payload: count - 1
})
  • 创建reducers
// reducer/counterReducer.js
const counterReducer = (state, action) => {
  switch(action.type) {
    case 'CHANGE_COUNT':
      return {
        ...state,
        count: action.payload
      }
    default:
      return state;
  }
};

export default counterReducer
  • 创建store
// Main.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

import App from './components/App.jsx';
import counterReducer from './reducers/counterReducer';

const initialState = {
  count: 0
};
const store = createStore(counterReducer, initialState);
render(
  <Provider store={store}>
    <App />
  </Provider>, 
  document.getElementById('root')
);
  • UI组件
// components/App.jsx
import React from 'react';
import { connect } from 'react-redux';
import * as actions from '../actions/actions';

class App extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    const {count, increment, decrement} = this.props;

    return (
      <div>
        <h1>The count is {count}</h1>
        <button onClick={() => increment(count)}>+</button>
        <button onClick={() => decrement(count)}>-</button>
      </div>
    );
  }
}

const mapStateToProps = store => ({
  count: store.count
});

const mapDispatchToProps = dispatch => ({
  increment: count => dispatch(actions.increment(count)),
  decrement: count => dispatch(actions.decrement(count))
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

用 React Hooks 来实现

  • 组织action,同上

  • 创建reducers,同上

  • 创建Context

// Main.js
import React, { createContext, useReducer } from 'react';
import {render} from 'react-dom';
import App from './components/App.jsx';
import reducer from './reducers/counterReducer';

const initialState = {
  count: 0
}
// 用 Context 实现类似 store 的全局容器
export const Context = createContext()

const Provider = props => {
   // 用 useReducer 生成 state 与 dispatch
    const [state, dispatch] = useReducer(reducer, initialState)
    return (
        <Context.Provider value={[state, dispatch]}>
            <App />
        </Context.Provider>
    )
}

render(
  <Provider />, 
  document.getElementById('root')
);
  • UI组件
// components/App.jsx
import React, { useContext } from 'react';
import { Context } from '../Main.js';
import * as actions from '../actions/actions';

const App = props => {
    // 用 useContext 来获取 state 与 dispatch
    const [state, dispatch] = useContext(Context)

    const increment = count => dispatch(actions.increment(count))
    const decrement = count => dispatch(actions.decrement(count))

    return (
      <div>
        <h1>The count is {state.count}</h1>
        <button onClick={() => increment(state.count)}>+</button>
        <button onClick={() => decrement(state.count)}>-</button>
      </div>
    );
}

export default App;

从上面代码中可以看到,React Hook主要是用到了useReduceruseContext两个hook,也有actionreducerdispatch,而且用法与Redux一样,只是store的实现、reducer的应用、state\dispatch的生成与获取方式有差异

数据流对比

Redux

ReduxDataFlow

Hooks

hooks-flow

重点看下 React Hooks,通过useReducer生成statedispatch,然后以Context做容器,在UI 组件内,通过useContext得到 statedispatch,主动调用 dispatch 发送 action,然后经过useReducer,触发reducer相应的数据改变。

它们都是在UI组件内调用dispatch发送action,然后经过reducer后,生成新的state回到UI组件更新视图,一样的数据流,只是实现有所不同

上面的例子很简单,React Hooks能实现Redux的基本功能,然而用到实际开发中,这还不够

组合reducer

实际开发中,我们经常需要根据页面组织不同的reducer,然后进行组合。

Redux中,有专门的工具combineReducers来实现,而React Hooks是没有的,我们需要自己实现

先看下,combineReducers干了啥

// todos、counter是两个reducer
import { combineReducers } from 'redux'
import counterReducer from './reducers/counterReducer';
import todosReducer from './reducers/todosReducer';

export default combineReducers({
  todos: counterReducer,
  counter: todosReducer,
})

返回一个调用 reducers 对象里所有 reducer 的 reducer,并且构造一个与 reducers 对象结构相同的 state 对象。

换一种说法,我们需要写一个函数combineReducers,使得下面的用法

const reducer = combineReducers({
  a: handleA,
  b: handleB,
  c: HandleC,
})

转换成

function reducer(state = {}, action) {
  return {
    a: handleA(state.a, action),
    b: handleB(state.b, action),
    c: HandleC(state.c, action)
  }
}

知道要干嘛就简单了,直接参照源码自己写一个

const combineReducers = reducers => {
    // 把非function的reducer过滤掉
    const finalReducers = Object.entries(reducers).reduce((res, [k, v]) => {
        if (typeof v === 'function') {
            res[k] = v
        }
        return res
    }, {})
    const finalReducersEntries = Object.entries(finalReducers)
    // 根据key调用每个reducer,将他们的值合并在一起
    return (state = {}, action) => {
        let hasChange = false
        const nextState = {}

        finalReducersEntries.forEach(([key, handle]) => {
            const previousValue = state[key]
            const nextValue = handle(previousValue, action)
            nextState[key] = nextValue
            hasChange = hasChange || previousValue !== nextValue
        })
        return hasChange ? nextState : state
    }
}

好了,一个简版的combineReducers就搞定了

更新下代码

// Main.js
...
import counterReducer from './reducers/counterReducer';
import todosReducer from './reducers/todosReducer';
import { combineReducers } from './util';

...
const reducers = combineReducers({
  counter: counterReducer,
  todos: todosReducer,
})

const Provider = props => {
    // 用 useReducer 生成 state 与 dispatch
    const [state, dispatch] = useReducer(reducers, initialState)
    return (
        <Context.Provider value={[state, dispatch]}>
            <App />
        </Context.Provider>
    )
}

...
// components/App.jsx
...

const App = props => {
    const [state, dispatch] = useContext(Context)
    const { counter } = state

    const increment = count => dispatch(actions.increment(count))
    const decrement = count => dispatch(actions.decrement(count))

    return (
      <div>
        <h1>The count is {counter.count}</h1>
        <button onClick={() => increment(counter.count)}>+</button>
        <button onClick={() => decrement(counter.count)}>-</button>
      </div>
    );
}

...

可以看到,UI组件获取用的state是全局的,而我们只需要state里面的counter,这样就会有个问题,只要全局state有更新,都会触发组件的重新渲染,还差点意思

状态切片

还是先看看Redux怎么处理的

import { createSelector } from 'reselect'
import { connect } from 'react-redux'

import App from './components/App'

export default connect(createSelector(
    state => ({ ...state.counter, ...state.common }),
    data => data,
))(Home)

createSelector重新组合state,生成可记忆的 Selector,然后connect连接到UI组件

重点看下createSelector,关键在于生成可记忆的 Selector

来看源码,摘重点

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // we reference arguments instead of spreading them for performance reasons
  return function () {
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null, arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}

可以看到,先缓存方法的入参及结果,然后每次调用的时候对比入参是否变化,如果没变化,就返回缓存的结果,原理很简单

我们试着写一个简版的

// useSelector.js

import { useContext, useMemo } from 'react'
// 这里我们其实只有一个`Context`,可以直接放这里吧
import { Context } from './index.js'

const useSelector = (...funcs) => {
    const [state, dispatch] = useContext(Context)
    console.log('useSelector', funcs)
    const resultFunc = useMemo(() => {
        if (funcs.length > 1) {
            return funcs.pop()
        }
        return ([v]) => v
    }, [funcs])
    const params = funcs.map(func => func(state))

    return useMemo(() => [resultFunc(params), dispatch], [resultFunc, params, dispatch])
}

const connect = (...args) => {
    return Cmp => {
        return props => {
            const [state, dispatch] = useSelector(...args)
            return useMemo(() => <Cmp {...props} {...state} dispatch={dispatch} /> , [props, state, dispatch])
        }
    }
}

export default connect

这里直接用useMemo实现了参数/结果的缓存处理,当然还有很多细节需要处理,先拿这个简版的试试效果

加到代码里面

// components/App.jsx
import React from 'react';
import * as actions from '../actions';
import { connect } from '../util'

const App = props => {
    // 用 useContext 来获取 state 与 dispatch
    const { count, dispatch } = props

    const increment = val => dispatch(actions.increment(val))
    const decrement = val => dispatch(actions.decrement(val))

    return (
      <div>
        <h1>The count is {count}</h1>
        <button onClick={() => increment(count)}>+</button>
        <button onClick={() => decrement(count)}>-</button>
      </div>
    );
}

export default connect(state => state.counter)(App);
...

运行代码,发现在App中调用increment依然会触发Todos组件的重新渲染,脑壳痛,一顿调试,才发现问题在这

// Main.js
...

const Provider = props => {
    // 用 useReducer 生成 state 与 dispatch
    const [state, dispatch] = useReducer(reducers, initialState)
    return (
        <Context.Provider value={[state, dispatch]}>
            <App />
            <Todos />
        </Context.Provider>
    )
}

...

更新dispatch的时候会导致useReducer创建新的state,触发Provider的重新渲染,从而导致AppTodos也会重新渲染

再优化下

// Provider.js
import React, { createContext, useReducer, useMemo } from 'react';

import counterReducer from './reducers/counterReducer';
import todosReducer from './reducers/todosReducer';
import { combineReducers } from './util';

// 用 Context 实现类似 store 的全局容器
export const Context = createContext()

const initialState = {
    counter: { count: 0 },
    todos: { text: 'test' },
}

const Provider = props => {
    const { children } = props

    const combinedReducer = combineReducers({
        counter: counterReducer,
        todos: todosReducer,
    })

    const [state, dispatch] = useReducer(combinedReducer, initialState)
    const context = useMemo(() => children, [children])
    return (
        <Context.Provider value={[state, dispatch]}>
            {context}
        </Context.Provider>
    )
}

export default Provider

// Main.js
import React from 'react';
import { render } from 'react-dom';
import App from './components/App.js';
import Todos from './components/Todos.js';
import Provider from './Provider';

render(
  <Provider>
      <Todos />
      <App />
  </Provider>,
  document.getElementById('root')
);

运行一下,完美,终于有了可以应用到开发中的样子,剩下的就是细节优化了,有时间再补吧~

总结

现在,来总结下,它们的异同点

相同点

  1. 可以实现统一状态管理
  2. 相同的actionreducerdispatch用法
  3. 一样的数据流

不同点

  1. React Hooks UI 层获取 statedispatch 是通过 useContext,而 Redux 是通过 HOC 依赖注入
  2. Reduxaction 之后改变视图本质上还是 state 注入的方式修改的组件内部 state,而 hooks 则是一对一的数据触发
  3. Reduxreducer 处理在 store 里面,而 React Hooks 则是通过 useReducer
  4. Redux 有很多为统一状态管理准备的工具,如combineReducersconnectcompose,还有各种middleware,生态也更加完善,相比React Hooks功能更加强大,开箱即用

我的理解

React Hooks 能替代 Redux 吗?结合上面的内容说下我的理解:

  • React Hooks不是替代Redux,而是提供了一个新的更灵活的状态管理选择

  • React HooksRedux语法更简洁、更优雅,能实现Redux的主要功能,也更加灵活。

    • 项目中有全局共享数据的需求,但是组件间数据交互并不是很复杂
    • 项目不需要全局共享数据,只是某个页面下多个非子组件间需要共享数据
    • 有复杂数据层级,需要复用的组件

    以上场景我觉得用React Hooks会更好

  • Redux有各种middleware,功能更强大,拓展性更好

    • 项目中有大量、复杂的网络请求,redux-saga
    • 项目需要全局状态管理,用Hooks满足不了需求

    以上场景我觉得用Redux会更好


或许,这是更优解

React-Redux 也拥有自己的Hook 了,参考 Clean Up Redux Code with React-Redux Hooks

useSelectorstore进行切片保存到具体组件,可以看出我们上面实现的hook很像,但是这个生成的还是普通的selector,还是需要配合createSelecter使用;

useDispatch更方便的获取dispatch

hook的语法,代替了HOC的依赖注入方式,更简洁,更优雅

// Main.js
import React from 'react';
import { createSelector } from 'reselect';
import * as actions from '../actions/actions';
import { useSelector, useDispatch } from 'react-redux';

const App = () => {
  const dispatch = useDispatch();
  const count = useSelector(createSelector(store => store.count, state => state));

  return (
    <div>
      <h1>The count is {count}</h1>
      <button onClick={() => dispatch(actions.increment(count))}>+</button>
      <button onClick={() => dispatch(actions.decrement(count))}>-</button>
    </div>
  );
}

export default App;

About

使用React Hooks 实现 Redux

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published