跳到主要内容

natur 使用手册

基本介绍

  1. 这是一个简洁、高效的react状态管理器
  2. 良好的typescript体验
  3. 单元测试覆盖率99%,放心使用
  4. 包体积,minizip 5k(uglify+gzip压缩后5k)
  5. 如果你的环境是react@17或者更低版本,你可以使用natur@2.1.x

起步

  1. 打开你的react项目
  2. 安装natur
yarn add natur
# npm install natur -S

设计

模块流程

模块

模块管理

natur本身是一个模块管理器,外加发布订阅

store

简单的示例

在线体验

声明模块

const count = {
// 存放数据
state: {
number: 0,
},
// state的映射
maps: {
isEven: ['number', number => number % 2 === 0],
},
// actions用来修改state
actions: {
inc: number => ({number: number + 1}),
dec: number => ({number: number - 1}),
}
}

创建store和inject

import { createStore, createUseInject } from 'natur';

const store = createStore({count}, {});
const useInject = createUseInject(() => store);
const useFlatInject = createUseInject(() => store, {flat: true});

在React中使用

// 创建一个count模块的注入器

// 声明props类型
const App = () => {
const [count] = useInject('count');
const [flatCount] = useFlatInject('count');
// flatCount中的数据是扁平的,即state、action、maps中的数据被放到一个对象中(state数据会被maps覆盖,请注意命名)

return (
<>
<button onClick={() => count.actions.dec(count.state.number)}>-</button>
<span>{count.state.number}</span>
<button onClick={() => count.actions.inc(count.state.number)}>+</button>
<span>{count.maps.isEven}</span>
</>
)
};

ReactDOM.render(<App />, document.querySelector('#app'));

module详解

一个模块由state, maps, actions构成

state — 存储数据

  • 必填:true
  • 类型:any
  • state用来存储数据

maps — 计算属性

  • 必填:false

  • 类型:{[map: string]: Array<string|Function> | Function;}

  • maps是state数据的映射,它的成员必须是一个数组或者函数,我们暂且称其为map

  • 如果map是数组,前面的元素都是在声明此map对state的依赖项。最后一个函数可以获取前面声明的依赖,你可以在里面实现你需要的计算的逻辑。在组件中,你可以获取数组最后一个函数运行的结果。

  • 如果map是函数,那么它只能接受state作为入参,或者没有参数,如果是state作为参数,那么当state更新时,此map一定会重新执行,没有缓存。如果map没有参数,那么此map只会执行一次

  • maps的结果是有缓存的,如果你声明的依赖项的值没有变化,那么最后一个函数便不会重新执行

  • 什么时候需要手动声明依赖?如果你的map逻辑较为复杂,或者你的map返回值不是基本类型的值,需要给到组件渲染,那么你可以考虑手动声明依赖,保证性能。一般直接使用函数的方式即可。

const demo = {
state: {
number: 1,
value: 2
},
maps: {
// 数组前面的元素,都是在声明此map对state的依赖项,最后一个函数可以获取前面声明的依赖,你可以在里面实现你想要的东西
sum1: ['number', 'value', (number, value) => number + value],
// 你也可以通过函数的方式声明依赖项,这对于复杂类型的state很有用
sum2: [state => state.number, s => s.value, (number, value) => number + value],
// 也可以是个函数,直接依赖整个state,缺点是只要state更新就会重新执行函数,没有缓存
sum3: ({number, value}) => number + value,
// 也可以是个函数,没有依赖,只执行一次
isTrue: () => true,
},
}
/**
* 在组件中你获得的数据为
* demo: {
* state: {
* number: 1,
* value: 2,
* }
* maps: {
* sum1: 3,
* sum2: 3,
* sum3: 3,
* isTrue: true
* }
* ...
* }
*/

actions — 更新数据

  • 必填:true
  • 类型:{[action: string]: (...arg: any[]) => any;}
  • actions的成员必须是函数,如果不设置中间件,那么它返回的任何数据都会作为新的state,并通知使用此模块的react组件更新,这是在natur内部完成的。
  • actions必须遵照immutable规范!
const demo = {
state: {
number: 1,
},
// actions用来修改state。它返回的数据会作为新的state(这部分由natur内部完成)
actions: {
inc: number => ({number: number + 1}),
dec: number => ({number: number - 1}),
}
}

/**
* 在组件中你获得的数据为
* demo: {
* state: {
* number: 1,
* }
* actions: {
* inc: (number) => 新的state,
* dec: (number) => 新的state,
* }
* ...
* }
*/

watch — 监听模块变动

  • 必填:false
  • 类型:{[moduleName: string]: (event: WatchEvent, api: WatchAPI) => void;} | (event: AllModuleEvent, api: WatchAPI) => void;
  • watch可以监听某个模块的变动,也可以监听所有模块的变动
  • 在watch里面可以获取当前模块的state,maps,也可以调用localDispatch来调用本模块的action
const demo = {
state: {
number: 1,
},
actions: {
inc: number => ({number: number + 1}),
dec: number => ({number: number - 1}),
},
watch: {
moduleA: (event: ModuleEvent, api: WatchAPI) => {
// 任何 moduleA 的变动都会触发这个函数,具体的变动信息在event参数获取
// 例如moduleA的初始化,更新state,销毁都可以监听到
// api参数包含demo模块的, getState, getMaps, localDispatch等API, 以及获取全局store的getStoreAPI.
// localDispatch只能调用本模块的action,例如:localDispatch('inc', 2);
}
}
}

const moduleA = {
state: {
number: 1,
},
actions: {
inc1: number => ({number: number + 1}),
dec1: number => ({number: number - 1}),
},
// watch也可以是一个函数用来监听所有模块的变动
watch: (event: AllModuleEvent, api: WatchAPI) => {
// 任何模块的变动都会触发这个函数,包括moduleA模块自己,具体的变动信息在event参数获取
// api参数包含moduleA模块的, getState, getMaps, localDispatch等API, 以及获取全局store的getStoreAPI.
// localDispatch是只能调用本模块的action,例如:localDispatch('inc1', 2);
}
}

应用场景

注入多个模块

// 导入你之前创建的inject函数,详情请参考上面的简单例子
import useInject from 'your-use-inject';

// 在你的组件中
const [module1] = useInject('module1');
const [module2] = useInject('module2');
// ...

同步更新数据

const app = {
state: {
name: "tom",
},
actions: {
// 这里是同步更新state中的name数据
changeName: newName => ({ name: newName }),
}
};

异步更新数据

const app = {
state: {
name: "tom",
},
actions: {
// 这里是异步更新state中的name数据
changeName: newName => Promise.resolve({ name: newName }),
}
};

异步多批次更新数据

import { ThunkParams } from "natur/dist/middlewares";

const state = {
now: Date.now(),
}
const actions = {
// 这里是异步多批次更新state中的name数据
updateNow: () => ({setState}: ThunkParams<typeof state>) => {
// 每秒更新一次now的值
setInterval(() => setState({now: Date.now()}), 1000);
},
}

const app = {
state,
actions
};

在actions中获取最新的state,maps值

import { ThunkParams } from "natur/dist/middlewares";

const state = {
name: 'tom',
}
const maps = {
nameIsTome: ['name', (name: string) => name === 'tom'],
}

const actions = {
updateName: () => ({getState, getMaps}: ThunkParams<typeof state, typeof maps>) => {
// 获取最新的state值
const currentState = getState();
// 获取最新的maps值
const currentMaps = getMaps();
},
}

const app = {
state,
maps,
actions
};

在actions中调用其他的action

import { ThunkParams } from "natur/dist/middlewares";

const state = {
name: 'tom',
loading: true,
}

const actions = {
loading: (loading: boolean) => ({loading}),
fetchData: (newName: string) => async ({localDispatch}: ThunkParams) => {
// 调用loading方法
localDispatch('loading', true);
await new Promise(resolve => setTimeout(resolve, 3000));
localDispatch('loading', false);
return {name: newName};
},
}

const app = {
state,
actions
};

组件只监听部分数据的变更

// 导入你之前创建的inject函数,详情请参考上面的简单例子
import useInject from 'your-use-inject';

// 这里App组件只会监听app,state中name的变化,其他值的变化不会引起App组件的更新
const [app] = useInject('app', {
state: ['name'], // 也可以使用函数声明 state: [s => s.name]
});

// 这里App组件只会监听app,maps中deepDep的变化,其他值的变化不会引起App组件的更新
const [app] = useInject('app', {
maps: ['deepDep'],
});
// 这里App组件不论app模块发生什么变化,都不会更新
const [app] = useInject('app', {});


// 因为actions在创建后会保持不变,所以你不必监听它的变化
const App = () => {
const [app] = useInject('app');
// 获取注入的app模块
const {state, actions, maps} = app;
return (
<input
value={state.name} // app中的数据
onChange={e => actions.changeName(e.target.value)}
/>
)
};


懒加载模块配置

/*
module1.js
export {
state: {
count: 1,
}
actions: {
inc: state => ({count: state.count + 1}),
}
}

*/
const otherLazyModules = {
// module2: () => import('module2');
// ...
}
const module1 = () => import('module1'); // 懒加载模块

// 创建store实例
// 第二参数就是懒加载的模块;
const store = createStore(
{ app },
{ module1, ...otherLazyModules }
);

// 然后用法等同于第二步

初始化state

import { createStore } from 'natur';
const app = {
state: {
name: 'tom',
},
actions: {
changeName: newName => ({ name: newName }),
asyncChangeName: newName => Promise.resolve({ name: newName }),
},
};
/*
createStore第三个参数
{
[moduleName: ModuleName]: Require<State>,
}
*/
const store = createStore(
{ app },
{},
);

store.globalSetStates({
app: {name: 'jerry'} // 设置app 模块的state
})

export default store;

跨模块的交互的复杂业务场景

你可以使用模块中的watch功能,他可以监听任何模块的任何变动,并且你可以发起你想要的dispatch

import { ModuleEvent, AllModuleEvent, WatchAPI } from 'natur';

export const moduleA = {
state: {},
actions: {/* ... */},
watch: {
moduleB(event: ModuleEvent, api: WatchAPI) {
// 任何 moduleB 的变动都会触发这个函数,具体的变动信息在event参数获取
// api参数包含本模块的, getState, getMaps, localDispatch等API, 以及获取全局store的getStoreAPI.
// localDispatch是只能调用本模块的action,例如:localDispatch('actionNameA', ...actionAArgs);
}
}
}
export const moduleB = {
state: {},
actions: {/* ... */},
// watch也可以是一个函数用来监听所有模块的变动
watch: (event: AllModuleEvent, api: WatchAPI) => {
// 任何模块的变动都会触发这个函数,具体的变动信息在event参数获取
// api参数包含本模块的, getState, getMaps, localDispatch等API, 以及获取全局store的getStoreAPI.
// localDispatch是只能调用本模块的action,例如:localDispatch('actionNameA', ...actionAArgs);
}
}

加载时占位组件配置

import { createInject } from 'natur';
// 全局配置
const inject = createInject({
storeGetter: () => store,
loadingComponent: () => <div>loading...</div>,
})
// 局部使用
inject('app')(App, () => <div>loading</div>);

在react之外使用natur

// 引入之前创建的store实例
import store from 'my-store-instance';

/*
获取注册的app模块, 等同于在react组件中获取的app模块
如果你想要获取懒加载的模块,
那么你必须确定,这个时候该模块已经加载好了
*/
const app = store.getModule('app');
/*
如果你确定,懒加载模块,还没有加载好
你可以监听懒加载模块,然后获取
*/
store.subscribe('lazyModuleName', () => {
const lazyModule = store.getModule('lazyModuleName');
});

/*
state: {
name: 'tom'
},
actions: {
changeName,
asyncChangeName,
},
maps: {
splitName: ['t', 'o', 'm'],
addName: lastName => state.name + lastName,
}
*/


/*
当你在这里使用action方法更新state时,
所有注入过app模块的组件都会更新,
并获取到最新的app模块中的数据,
建议不要滥用
*/
app.actions.changeName('jerry');
// 等同于
store.dispatch('app', 'changeName', 'jerry');

/**
*
* type: 模块变动的类型
* init: 模块初始化事件
* update: 模块state更新事件
* remove: 模块移除事件
*
* actionName: 模块更新state时的action名字
*/
type ModuleEvent = {
type: 'init' | 'update' | 'remove',
actionName?: string,
};
// 监听模块变动
const unsubscribe = store.subscribe('app', (me: ModuleEvent) => {
// 这里可以拿到最新的app数据
store.getModule('app');
});


// 取消监听
unsubscribe();

手动导入模块

// initStore.ts
import { createStore } from 'natur';

// 在实例化store的时候,没有导入懒加载模块
export default createStore({/*...modules*/}, {});

// ================================================
// lazyloadPage.ts 这是一个懒加载的页面
import store from 'initStore.ts'

const lazyLoadModule = {
state: {
name: 'tom',
},
actions: {
changeName: newName => ({ name: newName }),
},
maps: {
nameSplit: state => state.name.split(''),
addName: state => lastName => state.name + lastName,
},
};
/*
手动添加模块,在此模块被添加之前,其他地方无法使用此模块
要想其他地方也使用,则必须在store实例化的时候就导入
*/
store.setModule('lazyModuleName', lazyLoadModule);

const lazyLoadView = () => {
// 现在你可以获取手动添加的模块了
const {state, maps, actions} = store.getModule('lazyModuleName');
return (
<div>{state.name}</div>
)
}

dispatch

import { createStore, inject, InjectStoreModule } from 'natur';

const count = {
state: { // 存放数据
number: 0,
},
maps: { // state的映射。比如,我需要知道state中的number是否是偶数
isEven: ['number', number => number % 2 === 0],
},
actions: { // 用来修改state。返回的数据会作为新的state(这部分由natur内部完成)
inc: number => ({number: number + 1}),
dec: number => ({number: number - 1}),
}
}

// 创建store这一步需要在渲染组件之前完成,因为在组件中,需要用到你创建的store
const store = createStore({count}, {});

const {actions, state} = store.getModule('count')

actions.inc(state.number);
// 等于
store.dispatch('count', 'inc', state.number);

不想写那么多typescript代码? (NaturBaseFactory)

  • 不想写maps那么多类型?

    import { NaturBaseFactory } from 'natur';

    const state = {
    count: 1,
    };

    const createMap = NaturBaseFactory.mapCreator(state);
    const maps = {
    isOdd: createMap(
    // 这里的s无需手动声明类型
    s => s.count,
    // 这里的count无需手动声明类型,自动推导出前面的返回数据类型
    count => count % 2 === 1
    )
    }
  • 不想写actions类型?

    一般插件都建议重写此类方法,以符合插件的类型提示,例如natur-immer的NaturFactory.actionsCreator

    import { NaturBaseFactory } from 'natur';

    const state = {
    count: 1,
    };

    const createMap = NaturBaseFactory.mapCreator(state);

    const maps = {
    isOdd: createMap(
    s => s.count,
    count => count % 2 === 1
    )
    }

    // 第二个参数是可选的,没有maps可以不用传
    const createActions = NaturBaseFactory.actionsCreator(state, maps);

    const actions = createActions({
    // 这里的api类型会自动提示,不需要再手动声明
    updateCount: (count: number) => api => {
    api.setState(count)
    }
    })
  • 不想写watch

    import { NaturBaseFactory } from 'natur';
    /**
    * 第一个参数是你想
    */
    const createWatch = NaturBaseFactory.watchCreator(module1, state, maps);
    const watch = createWatch({
    // event和api的类型都会自动推导出,无需手动声明
    module1Name: (event, api) => {
    // xxx
    }
    })

拦截器

在模块调用action或者store.dispatch时会先经过interceptor,因此拦截器可以应用于,控制action是否执行,以及action的入参控制等场景

import {
createStore,
Interceptor
InterceptorActionRecord,
InterceptorNext,
InterceptorParams,
} from 'natur';

const app = {
state: {
name: 'tom',
},
actions: {
changeName: newName => ({ name: newName }),
asyncChangeName: newName => Promise.resolve({ name: newName }),
},
};


type InterceptorActionRecord = {
moduleName: String;
actionName: String;
actionArgs: any[];
actionFunc: (...arg: any) => any; // 原始的action方法
}

type InterceptorNext = (record: InterceptorActionRecord) => ReturnType<Action>;

// InterceptorParams类型于MiddlewareParams类型相同
type InterceptorParams = {
setState: MiddlewareNext,
getState: () => State,
getMaps: () => InjectMaps,
dispatch: (action, ...arg: any[]) => ReturnType<Action>,
};

const LogInterceptor: Interceptor<typeof store.type> = (interceptorParams) =>
(next: InterceptorNext) =>
(record: InterceptorActionRecord) => {
console.log(`${record.moduleName}: ${record.actionName}`, record.actionArgs);
return next(record); // 你应该return, 只有这样你在页面调用action的时候才会有返回值
};
const store = createStore(
{ app },
{},
{
interceptors: [LogInterceptor, /* ...moreInterceptor */]
}
);

export default store;

中间件

中间件的执行发生在action执行之后,更新state之前。可以接收action的返回值,一般可以应用于action返回值的加工,state更新的控制等行为

import {
createStore,
MiddleWare,
MiddlewareNext,
MiddlewareActionRecord
} from 'natur';

const app = {
state: {
name: 'tom',
},
actions: {
changeName: newName => ({ name: newName }),
asyncChangeName: newName => Promise.resolve({ name: newName }),
},
};

type MiddlewareActionRecord = {
moduleName: String,
actionName: String,
state: ReturnType<Action>,
}

type MiddlewareNext = (record: MiddlewareActionRecord) => ReturnType<Action>;

type middlewareParams = {
setState: MiddlewareNext,
getState: () => State,
getMaps: () => InjectMaps,
dispatch: (action, ...arg: any[]) => ReturnType<Action>,
};

const LogMiddleware: MiddleWare<typeof store.type> = (middlewareParams) =>
(next: MiddlewareNext) =>
(record: MiddlewareActionRecord) => {
console.log(`${record.moduleName}: ${record.actionName}`, record.state);
return next(record); // 你应该return, 只有这样你在页面调用action的时候才会有返回值
// return middlewareParams.setState(record); // 你应该return,只有这样你在页面调用action的时候才会有返回值
};
const store = createStore(
{ app },
{},
{
middlewares: [LogMiddleware, /* ...moreMiddleware */]
}
);

export default store;

内置中间件说明

thunkMiddleware: thunk中间件可以使得action可以返回函数,拥有了获取最新的state,maps,以及setState,dispatch等增强功能

如果你喜欢mutable的写法,推荐使用natur-immer

import { thunkMiddleware, ThunkParams } from 'natur/dist/middlewares'

const actionExample = (myParams: any) => ({
getState,
setState,
getMaps,
dispatch
}: ThunkParams<typeof stateOfThisModule, typeof mapsOfThisModule>) => {
const currentState = getState(); // 最新的state
const currentMaps = getMaps(); // 最新的maps
// dispatch('otherActionNameOfThisModule', ...params)
// dispatch('otherModuleName/otherActionNameOfOtherModule', ...params);
setState(currentState); // 更新state
return currentState; // 更新state
}

promiseMiddleware: action支持异步操作

// promiseMiddleware
const action1 = () => Promise.resolve(2333);
const action2 = async () => await new Promise(res => res(2333));

fillObjectRestDataMiddleware: state增量更新/覆盖更新,state是对象时才有效

const state = {
a: 1,
b: 2
};
/**
* 调用此action,最后的state是{a: 11, b:2}此中间件要求
* state和action返回的数据必须都是普通对象
*/
const action = () => ({a: 11})

shallowEqualMiddleware:浅层比较优化中间件,仅限于普通对象的state

const state = {
a: 1,
b: 2
};
const action = () => ({a: 1, b:2}) // 与旧的state相同,不做更新视图

filterUndefinedMiddleware: 过滤返回undefined的action操作

const action = () => undefined; // 这种action的返回不会作为新的state

devtool:开发调试工具

// redux.devtool.middleware.ts
import { Middleware } from 'natur';
import { createStore } from 'redux';

const root = (state: Object = {}, actions: any):Object => ({
...state,
...actions.state,
});

const createMiddleware = ():Middleware => {
if (process.env.NODE_ENV === 'development' && (window as any).__REDUX_DEVTOOLS_EXTENSION__) {
const devMiddleware = (window as any).__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(root, devMiddleware);
return ({getState}) => next => record => {
store.dispatch({
type: `${record.moduleName}/${record.actionName}`,
state: {
[record.moduleName]: record.state || getState(),
},
});
return next(record);
}
}
return () => next => record => next(record);
}

export default createMiddleware();

推荐的中间件配置

注意:中间件配置的先后顺序很重要


import {createStore} from 'natur';
import {
thunkMiddleware,
promiseMiddleware,
fillObjectRestDataMiddleware,
shallowEqualMiddleware,
filterUndefinedMiddleware,
} from 'natur/dist/middlewares';
import devTool from 'redux.devtool.middleware';

const store = createStore(
modules,
{},
{
middlewares: [
thunkMiddleware, // action支持返回函数,并获取最新数据
promiseMiddleware, // action支持异步操作
fillObjectRestDataMiddleware, // 增量更新/覆盖更新
shallowEqualMiddleware, // 新旧state浅层对比优化
filterUndefinedMiddleware, // 过滤无返回值的action
devTool, // 开发工具
],
}

);

typescript支持

基础用法

import React from 'react';
import ReactDOM from 'react-dom';
// 导入你之前创建的inject函数,详情请参考上面的简单例子
import inject from 'your-inject';
import {ModuleType, Store} from 'natur';

const count = {
state: { // 存放数据
number: 0,
},
maps: { // state的映射。比如,我需要知道state中的number是否是偶数
isEven: ['number', number => number % 2 === 0],
},
actions: { // 用来修改state。返回的数据会作为新的state(这部分由natur内部完成)
inc: number => ({number: number + 1}),
dec: number => ({number: number - 1}),
}
}



// 生成count模块在组件中获得的类型
type InjectCountType = ModuleType<typeof count>;

const injector = inject('count');

type otherProps = {
className: string,
style: Object,
}

const App: React.FC<typeof injector.type & otherProps> = (props) => {
const {state, actions, maps} = props.count;
return (
<>
<button onClick={() => actions.inc(state)}>+</button>
<span>{state.count}</span>
<button onClick={() => actions.dec(state)}>-</button>
</>
)
}

const IApp = injector(App);

const app = (
<IApp className='1' style={{}} />
);
ReactDOM.render(
app,
document.querySelector('#app')
);

重新定义store类型

import {Store, createStore} from 'natur';

const count = {
/* ... */
}

const lazyModule1 = () => import(/* ... */);

const allSyncModules = {
count,
/* and others */
}
const allAsyncModules = {
lazyModule1,
/* and others */
}

const store = createStore(allSyncModules, allAsyncModules);

type StoreInsType = Store<typeof allSyncModules, typeof allAsyncModules>;

// StoreInsType的类型就是store的类型,你可以扩展你的类型

为什么选择NATUR

系统设计理念

  1. natur的初衷是简单自然的掌管项目中所有的业务逻辑,在这方面是不同于redux或者mobx这样的状态管理库。natur可以轻松的让项目中所有的业务与UI层松耦合,这可以让UI保持足够的简单和纯粹,对于项目的维护性有着很大的好处。
  2. 模块的state包含了业务数据的存储。maps包含了state衍生数据的逻辑,以及缓存的设计保证性能,值得一提的是,maps使用了手动声明依赖的方式,这也与是react一贯的设计风格相符合。action包含了state数据更新,以及其他业务逻辑(比如一个没有返回值的API的调用),natur推荐action设计的职责明确,一个action只做一件事原则。如果能让每个action的执行有对应state变化,那么这能够让整个项目具有可观测和追踪性,并且可以更好的为模块间的通讯服务(可观测和追踪性可以通过拦截器中间件来实现)。
  3. 在模块通讯这里,watch可以很好的监听模块动作,并解耦模块之间依赖
  4. natur模块的设计方面,则是推荐用户细分模块,明确模块的边界,以及粒度,以保证模块的可维护性。确保一个natur模块中只处理自己的业务,而不需要关心其他模块,与别的模块没有耦合。
  5. 因为natur模块的创建和使用方式足够的简单,所以使得开发在设计方面能够尽可能简单自然的写出符合natur设计理念的项目,当然最主要的还是开发人员需要明白natur的设计理念并遵循它。

与redux对比

  1. 首先redux是全局状态管理器,这与natur的项目业务逻辑管理的设计目标是不同的。
  2. 其次redux的使用成本比较高,natur使用则是非常的简单
  3. 性能方面,在natur中存在缓存、模块懒加载,以及部分监听功能的支持,你不需要额外的库来保证你的项目性能。

与mobx的对比

  1. 兼容性方面,因为mobx使用了proxy或者definePropertyAPI所以兼容性要稍微差点
  2. 在模块通讯方面,mobx的设计无法完美结耦,这个是一个遗憾
  3. 性能方面,mobx的缓存性能优化依赖immutable所以在使用友好性方面较于natur更好, 但是natur也有着模块懒加载这样的功能优于mobx

使用注意事项

  • 由于低版本不支持react.forwardRef方法,所以不能直接使用ref获取包裹的组件实例,需要使用forwardedRef属性获取(用法同ref)

  • 在TypeScript中的提示可能不那么友好,比如

@inject('count', 'name')
class App extends React.Component {
// ...
}

// 此使用方法会报错,提示App组件中无forwardedRef属性声明
<App forwardedRef={console.log} />

// 以下使用方式则不会报错
class _App extends React.Component {
// ...
}
const App = inject('count', 'name')(_App);
// 正确
<App forwardedRef={console.log} />
  • 在actions中修改state,需要遵循immutable规范

快速开始

2 分钟即可上手:

yarn add natur
import React from 'react';
import { createStore, createUseInject } from 'natur';

// 1. 定义模块
const counter = {
state: 0,
actions: {
inc: (s) => s + 1,
dec: (s) => s - 1,
},
};

// 2. 创建 store 和 inject hook
const store = createStore({ counter }, {});
const useInject = createUseInject(() => store);

// 3. 在组件中使用
function Counter() {
const [counter] = useInject('counter');
return (
<div>
<button onClick={() => counter.actions.dec(counter.state)}>-</button>
<span>{counter.state}</span>
<button onClick={() => counter.actions.inc(counter.state)}>+</button>
</div>
);
}

错误处理

当 action 返回一个 rejected Promise 时,natur 不会捕获错误。你需要在 action 中自行处理:

const module = {
state: { data: null, error: null },
actions: {
fetchData: async () => {
try {
const data = await api.getData();
return { data, error: null };
} catch (e) {
return { error: e.message };
}
},
},
};

React 错误边界的使用方式与平时一致:

<ErrorBoundary>
<InjectedComponent />
</ErrorBoundary>

测试

通过独立测试 action 来对 natur 模块进行单元测试:

import { createStore } from 'natur';

test('counter inc action', () => {
const store = createStore({
counter: {
state: 0,
actions: { inc: (s) => s + 1 },
},
}, {});

store.dispatch('counter', 'inc', undefined);
const counter = store.getModule('counter');
expect(counter.state).toBe(1);
});

测试使用 useInject 的 React 组件时,创建一个测试专用 store 并包裹组件:

import { render, screen } from '@testing-library/react';

test('Counter renders state', () => {
const store = createStore({ counter: { state: 5, actions: {} } }, {});
const useInject = createUseInject(() => store);
// 使用此 store 渲染组件...
});

SSR(服务端渲染)

natur 通过 Provider 组件支持 SSR。对于 Next.js 或其他 SSR 框架:

// 服务端:每个请求创建新的 store
function createServerStore() {
const store = createStore(modules, lazyModules);
await store.dispatch('user', 'fetchUserData');
return store.getAllStates();
}

// 客户端:使用服务端状态 hydration
import { Provider } from 'natur';

function App({ serverState }) {
const store = createStore(modules, lazyModules);
if (serverState) {
store.globalSetStates(serverState);
}
return (
<Provider store={store}>
<YourComponents />
</Provider>
);
}

使用 umi-natur 时,插件会自动处理状态序列化。

常见问题

应该使用哪个插件?

先从 natur 核心库开始。根据需要添加插件:

  • 需要页面刷新后保留数据?→ natur-persist
  • 更喜欢可变风格的状态更新?→ natur-immer
  • 开发 React Native 应用?→ natur-persist-async
  • 使用 Umi 框架?→ umi-natur

natur 与 Zustand / Jotai 有何不同?

naturZustandJotai
架构模块化(state+maps+actions)单一 store hook原子化
TypeScript一等支持,模块类型自动推导良好良好
体积~5KB~1KB~2KB
中间件内置拦截器+中间件系统中间件
插件官方 persist/immer/umi 插件社区社区

natur 在具有多个业务域的大型应用中表现更优,得益于其内置的模块系统和插件生态。

natur 支持 Next.js 吗?

支持。使用 Provider 组件和 globalSetStates 进行 SSR hydration。详见SSR 章节

如何从 Redux 迁移?

  1. 替换 slices → natur 模块(state + actions
  2. 替换 useSelectoruseInject('moduleName')
  3. 替换 dispatchstore.dispatch('moduleName', 'actionName', ...args)
  4. 替换中间件 → natur 中间件(或使用 natur-immer 获得类 Redux Toolkit 的 immer 支持)

action 必须是纯函数吗?

不使用中间件时,action 应为纯函数——其返回值直接作为新的 state。使用 thunk 中间件后,action 可以是异步的,且可以使用 getState/setState/localDispatch

使用注意事项(扩展)

  • 必须遵循 immutable 规范:action 必须返回新的 state 对象,不能直接修改现有 state。如果喜欢可变风格,请使用 natur-immer
  • 懒加载模块的访问:懒加载模块在首次使用前不会被加载。在加载完成前调用 store.getModule('lazyModule') 会返回 undefined。请先使用 store.loadModule()
  • Provider 嵌套:如果使用多个 <Provider> 组件,最近的祖先 Provider 会优先被 useInject/useStore 使用。
  • Flat inject 命名冲突useFlatInject 中,state 的 key 优先级高于 maps 的 key。请避免 state 和 maps 属性之间的命名冲突。
  • useInject 返回数组:始终使用解构:const [module] = useInject('name')。第二个元素是 store 引用,这与 React 的 useState 不同(useState 的第二个元素是 setter)。