react使用ctx和reducer代替redux

入门版本

创建一个store,包含ctx、reduce、dispatch+action

import { createContext, useContext } from 'react';

// 定义ctx
export const defaultValue = {
  count: 0,
};
export const AppCtx = createContext(null);

export function useAppCtx() {
  const ctx = useContext(AppCtx);
  return ctx;
}

// 定义reducer
export function appReducer(app, action) {
  switch (action.type) {
    case 'update': {
      return { ...app, ...action.payload };
    }
    case 'clear': {
      return {};
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

// 定义dispatch和action(省的在组件手写dispatch)
export const appCtxDispatch = {
  update: (dispatch, payload) => {
    dispatch({
      type: 'update',
      payload,
    });
  },
  clear: (dispatch) => {
   dispatch({
      type: 'clear',
    })
  },
};

最后我们在组件中使用

import { memo, useReducer } from 'react';
import { AppCtx, appReducer, defaultValue, useAppCtx, appCtxDispatch } from '@/hooks/useAppCtx';

// 子组件
const Demo = memo(() => {
  console.log('Demo render');
  const { app } = useAppCtx();

  return (
    <div>
      我是子组件
      <div>{JSON.stringify(app)}</div>
    </div>
  );
});

// 父组件
const App = () => {
  console.log('App render');

  const [app, dispatch] = useReducer(appReducer, defaultValue);
  const doInc = () => {
    appCtxDispatch.update(dispatch, {
      count: app.count + 1,
    });
  };

  return (
    <AppCtx value={app}>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变组件
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

优化版本

解决无辜组件的不必要的重新render问题

// app-store.ts
import { produce } from 'immer';
import { createContext, useContext } from 'react';

// 定义ctx
export const defaultValue = {
  count: 0,
};
export const AppCtx = createContext(null);

export function useAppCtx() {
  const ctx = useContext(AppCtx);
  return ctx;
}

// 定义reducer
// 优化1: 使用immer,防止payload内容不变,而返回全新的对象导致 使用的地方被rerender(即固化)
export function appReducer(app, action) {
  return produce(app, (draft) => {
    switch (action.type) {
      case 'update': {
        Object.assign(draft, action.payload); // 只真正改动的字段会触发草稿变更
        return; // 没改动时 Immer 自动返回旧引用
      }
      case 'clear': {
        return {};
      }
      default:
        throw new Error('Unknown action: ' + action.type);
    }
  });
}

// 定义dispatch和action(省的在组件手写dispatch)
export const appCtxDispatch = {
  update: (dispatch, payload) => {
    dispatch({
      type: 'update',
      payload,
    });
  },
  clear: (dispatch) => {
    dispatch({
      type: 'clear',
    });
  },
};

组件中使用

import { memo, useMemo, useReducer } from 'react';
import { AppCtx, appReducer, defaultValue, useAppCtx, appCtxDispatch } from '@/hooks/useAppCtx';

// 子组件
const Demo = memo(() => {
  console.log('Demo render');
  const { app } = useAppCtx();

  return (
    <div>
      我是子组件
      <div>{JSON.stringify(app)}</div>
    </div>
  );
});

// 父组件
const App = () => {
  console.log('App render');

  const [app, dispatch] = useReducer(appReducer, defaultValue);
  const doInc = () => {
    appCtxDispatch.update(dispatch, {
      count: app.count+1,
    });
  };

  // 优化2: 防止App本身render,导致ctx的值被迫改变(即固化)
  const appCtxValue = useMemo(() => ({ app, dispatch }), [app]); 

  return (
    <AppCtx value={appCtxValue}>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变组件
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

简化版本

其实仅仅定义ctx,然后把state的定义和更新作为(ctx provider所在的)父组件的普通状态。
我们也可以做到一个store的能力!

// use-app-ctx.ts
import { Updater } from 'use-immer'
import { createContext, useContext } from 'react';

export type AppCtxProps = {
  count: number
  setApp: Updater<AppCtxProps>
}

// 定义ctx
export const defaultValue = {
  count: 0,
  setApp: () => {}
};
export const AppCtx = createContext<AppCtxProps>(defaultValue);

export function useAppCtx() {
  const ctx = useContext(AppCtx);
  return ctx;
}

然后在组件中使用即可

import { memo, useMemo } from 'react';
import { AppCtx, defaultValue, useAppCtx } from '@/hooks/useAppCtx';
import {useImmer} from  'use-immer'

// 子组件
const Demo = memo(() => {
  console.log('Demo render');
  const { count } = useAppCtx();

  return (
    <div>
      我是子组件
      <div>{count}</div>
    </div>
  );
});

// 父组件
const App = () => {
  console.log('App render');

  const [app, setApp] = useImmer(defaultValue); 
  const doInc = () => {
    // useImmer,会仅仅当内容发生变化时才会设置为新对象,否则依然是原对象 (即固化)
    setApp(draft => { 
      draft.count = draft.count+1;
    });
  };
  
  const appCtxValue = useMemo(() => ({ ...app, setApp }), [app]) 
  return (
    <AppCtx value={ appCtxValue }>
      <div>{app.count}</div>
      <button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
        改变组件
      </button>
      <hr className="mt-2" />
      <Demo />
    </AppCtx>
  );
};

export default App;

假设你没有更复杂的action,仅仅只有set,那这样也是推荐的方案!

可能需要?

/* 真正固化:依赖空数组,引用永不变 */
  const stableSetApp = useCallback<typeof setApp>(
    (recipe) => setApp(recipe),
    []          // 空依赖 => 跨渲染不变
  );
  const appCtxValue = useMemo(() => ({ ...app, setApp: stableSetApp }), [app]) 

其它

ctx provider的使用最好精确到使用位置,不要什么都在顶部放,
以及 ctx state也别什么都放,
假设当到了根组件app上使用了且还随意放了很多状态:那只要ctx内容变了,所有子组件及其孙子子子子都会rerender,后期我们只能通过memo来包裹来避免!

react的原则是,跨组件都要用到的共性数据,方可提取ctx!

posted @ 2025-09-24 16:04  丁少华  阅读(3)  评论(0)    收藏  举报