react hooks 全面轉換攻略(三) 全局存儲解決方案

針對 react hooks 的新版本解決方案

一.redux維持原方案

若想要無縫使用原來的 redux,和其配套的中間件 promise,thunk,saga 等等的話
可以使用 redux-react-hook

github 鏈接 redux-react-hook

一個簡單的使用例子:

import {useDispatch, useMappedState} from 'redux-react-hook';

export function DeleteButton({index}) {
  // 類似於以前 react-redux 中的 connect 函數
  const mapState = useCallback(
    state => ({
      canDelete: state.todos[index].canDelete,
      name: state.todos[index].name,
    }),
    [index],
  );

  // 獲取 redux 的數據
  const {canDelete, name} = useMappedState(mapState);

  // 獲取 dispatch 
  const dispatch = useDispatch();
  
  
  // button click handle
  const deleteTodo = useCallback(
    () =>
      dispatch({
        type: 'delete todo',
        index,
      }),
    [index],
  );

  return (
    <button disabled={!canDelete} onClick={deleteTodo}>
      Delete {name}
    </button>
  );
}

使用方法和以前一致

二.使用 useReducer 與 context

在 index 或 app 中提供全局的 redux 與 dispatch

function isPromise(obj) {
  return (
    !!obj &&
    (typeof obj === "object" || typeof obj === "function") &&
    typeof obj.then === "function"
  );
}

function wrapperDispatch(dispatch) {
  // 功能和 redux-promise 相同
  return function (action) {
    isPromise(action.payload) ?
      action.payload.then(v => {
        dispatch({type: action.type, payload: v})
      }).catch((error) => {
        dispatch(Object.assign({}, action, {
          payload: error,
          error: true
        }));
        return Promise.reject(error);
      })
      :
      dispatch(action);
  };
}


function Wrap(props) {
  // 確保在 dispatch 後不會刷新APP組件
  const [state, dispatch] = useReducer(reducers, ReducersValue);
  console.log('render wrap')
  return (<MainContext.Provider value={{state: state, dispatch: wrapperDispatch(dispatch)}}>{props.children}</MainContext.Provider>)
}

function App() {
  console.log('render  App')
  return <Wrap>
    <Router>
      <Switch>
        <Route path="/login" component={Login} exact/>
        <Route path="/" component={MainIndex}/>
      </Switch>
    </Router>
  </Wrap>
}

具體使用:

function useDispatch() {
  // 獲取 dispatch
  const store = useContext(MainContext);
  return store.dispatch;
}

function useStoreState(mapState) {
  //存儲 state 且判斷是否需要 render
  const {state:store} = useContext(MainContext);

  const mapStateFn = () => mapState(store);

  const [mappedState, setMappedState] = useState(() => mapStateFn());

  const lastRenderedMappedState = useRef();
  // Set the last mapped state after rendering.
  useEffect(() => {
    lastRenderedMappedState.current = mappedState;
  });

  useEffect(
    () => {
     console.log('useEffect ')
      const checkForUpdates = () => {
        const newMappedState = mapStateFn();
        if (!_.isEqual(newMappedState, lastRenderedMappedState.current)) {
          setMappedState(newMappedState);
        }
      };

      checkForUpdates();
    },
    [store, mapState],
  );
  return mappedState
}

// 組件內使用
const ResourceReducer = useStoreState(state => state.ResourceReducer)
const dispatch = useDispatch()

他的功能已經足夠了,在使用的地方使用函數即可,很方便
但是也有一些不足的地方是在根源上的,即 context,
在同一個頁面中 如果有多個使用 context 的地方
那麼如果一旦dispatch ,其他的所有地方也會觸發render 造成資源的浪費,小項目還好,大項目仍舊不可

(除非 react 的 context 函數添加 deps)

三.自定義解決方案

原理就是存儲一個全局變量 ,通過 import 引入;
我自己寫了一個例子:https://github.com/Grewer/react-hooks-store
想要基礎的實現只需要 30+ 行的代碼即可

class Modal {
  private value: any;
  private prevValue: any;
  private reducers: (state, action) => {};
  private queue: any = [];
  private dispatch: (action) => void;

  constructor(reducers) {
    this.reducers = combineReducers(reducers)
    // combineReducers 來自於 reudx ,可以引入也可以自己寫一個(後續我會寫一個庫,會包含此函數)
    this.value = this.reducers({}, {})
    this.dispatch = action => {
      this.prevValue = this.value;
      this.value = this.reducers(this.value, action)
      this.onDataChange()
    }
  }

  useModal = (deps?: string[]) => {
    const [, setState] = useState(this.value);
    useEffect(() => {
      const index = this.queue.push({setState, deps}); // 訂閱
      return () => { // 組件銷燬時取消
        this.queue.splice(index - 1, 1);
      };
    }, []);
    return [this.value, this.dispatch]
  }

  onDataChange = () => {
    this.queue.forEach((queue) => {
      const isRender = queue.deps ? queue.deps.some(dep => this.prevValue[dep] !== this.value[dep]) : true
      isRender && queue.setState(this.value)
    });
  }
}

// 初始化 reducers

const modal = new Modal({
  countReducer: function (state = 0, action) {
    console.log('count Reducer', state, action)
    switch (action.type) {
      case "ADD":
        console.log('trigger')
        return state + action.payload || 1
      default:
        return state
    }
  },
  listReducer: function (state = [] as any, action) {
    console.log('list Reducer', state, action)
    switch (action.type) {
      case "ADD_LIST":
        console.log('trigger')
        state.push(action.payload)
        return [...state]
      default:
        return state
    }
  },
  personReducer: function (state = {name: 'lll', age: 18} as any, action) {
    console.log('person Reducer', state, action)
    switch (action.type) {
      case "CHANGE_NAME":
        return Object.assign({}, state, {name: action.payload})
      default:
        return state
    }
  }
})
// 導出 useModal
export const useModal = modal.useModal

簡單的使用:

function Count(props) {
  const [state, dispatch] = useModal(['countReducer'])
  // 非 countReducer 的更新 不會觸發此函數 render
  console.warn('render Count', state, dispatch)

  return <div>
    <button onClick={() => dispatch({type: "ADD", payload: 2})}>+</button>
  </div>
}

當然你也可以自己寫一個,自己想要的方案

總結

hooks 的存儲方案基本就這 3 類,可以用現成的,也可以使用自己寫的方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章