0.0.9 • Published 3 years ago

make-state v0.0.9

Weekly downloads
-
License
MIT
Repository
-
Last release
3 years ago

make-state

简单灵活的React状态库,基于并兼容Recoil。

特点&动机:

Recoil作为新一代状态管理非常好用,不过稍有点复杂,API略多。受jotai的灵感,简化API,提高易用性:

  • 简单的API,类似useState的用法,无需学习Recoil,5分钟上手。
  • 可以和Recoil共用,支持Recoil几乎所有特性,满足深度使用场景。

极简示例(CodeSandbox):

import ReactDom from 'react-dom'
import makeState, {RecoilRoot} from 'make-state'

const [useCount] = makeState(0) // 创建状态

function Demo () {
    const [count, setCount] = useCount() // 使用状态, 类似useState

    return <div>
        {count /* 读 */}
        <button onClick = {() => {
            setCount(state => state + 1) // 写
        }}> +1
        </button>
    </div>

}

ReactDom.render(
    <RecoilRoot> {/*包裹组件,类似Provider*/}
      <Demo>
    </RecoilRoot>,
    document.getElementById('root)
)

快速起步

我们将制作一共可以显示字符长度,并拥有删除按钮的文本框。(CodeSandBox)。

1. 安装

npm i make-state --save

// or yarn
yarn add make-state

2. 使用RecoilRoot包裹React组件

使用<RecoilRoot>包裹应用中的其他组件, 推荐将<RecoilRoot>放置在根组件:

import {RecoilRoot} from 'make-state'

ReactDOM.render(
  <ReocilRoot >
	{/* 其他内容 */}
  </ReocilRoot >
  document.getElementById('root')
)

make-state和Recoil中的RecoilRoot完全一致。如果您的项目已使用Recoil,并已添加RecoilRoot,无需此步。

3. 创建原始状态(primary state)

首先创建原始状态,原始状态可以是数字、字符串、对象等类型,可以被多个组件共用。makeState有两个返回值,第一个是类似useState的hook,第二个是原始状态。

import makeState from 'make-state'

const [useText, textState] = makeState("hello world")

// makeState初始值支持多种类型, 如:
const [useName] = makeState('Tom')
const [useNames] = makeState(['Tom', 'Jerry'])
const [useUser] = makeState({name: 'Tom'})

如果你了解Recoil,会发现makeState生成的状态同atom的返回值一样,是一个RecoilState。

4. 在组件中使用

makeState返回的useText和React的useState几乎一样:useText返回两个值,第一个是状态,第二个是set函数。

const Text = () => {
  const [text, setText] = useText()

  return (
    <input
      value={text /* 读 */} 
      onChange={(e) => {
        setText(e.target.value) // 写
      }}
    />
  )
}

执行setText后,所有用到textState的组件都将重新渲染,并返回最新的textState

5. 创建驱动状态(drived stated)

通过对原始状态的计算,会产生一共个的状态,将其称为驱动状态(或者称作计算属性,computed data)。向makeState传入两个参数,将产生驱动状态。第一个参数是读值函数,通过传参get可以获取原始状态、其他驱动状态,第二个参数是写值函数。

下面的例子通过驱动状态计算文本长度,由于不需要写值,第二个参数传null,表示只读驱动状态。 textState是前文makeState('hello world)生成的状态。

const [useTextLen] = makeState(
  (get) => get(textState).length, // 计算取值
  null // 别忘了null
)

const TextLen = () => {
  const [len] = useTextLen() // 驱动状态的使用方法和原始状态一样

  return <p>Length: {len} </p>
}

如果你了解Recoil,会发现此时生成的状态同selector的返回值一样,也是一个RecoilState。

6. 创建可写驱动状态(writeable drrived stated)

makeState中第二个参数传入函数,将生成一个可写驱动状态。函数中的参数 get获取其他状态值, set修改状态值,newValue是新传入的值。

使用可写驱动函数,删除文本的最后N位数:

const [useDelete] = makeState(
  get => get(text),
  (get, set, newValue) => {
    set(textState, // 要修改的状态
      text => text.slice(0, text.length - newValue)) // 删除最后N(newValue)位
  }
)

const DeleteBtn = () => {
  const [, sliceText] = useDelete()

  return (
    <button
      onClick={() => {
        sliceText(1) // 删除最后1个字符,传入2则删除最后2个字符
      }}
    >
      delete
    </button>
  )
}

(如果你了解Recoil,会发现此时生成的状态同selector(write)的返回值一样,是一个RecoilState)

Recipes

数据流

利用驱动函数,可以将一共状态转为另一个状态,像流水线一样,形成数据流。一个状态的变动会引起整个数据流自动变化(CodeSandBox):

const [useCount1, count1] = makeState(1);
const [useCount2, count2] = makeState(2);
const [useCount3, count3] = makeState(3);

const [useSum, sum] = makeState(
  (get) => get(count1) + get(count2) + get(count3), null
);

const [useSumDouble] = makeState((get) => get(sum) * 2, null);

count1, count2等,只要有一个状态变更, sumDouble会自动变更。

异步与请求数据(Suspense)

可以在makeState中传入异步函数,请求数据,例如查询HackerNews中的评论(CodeSandBox):

import makeState, { RecoilRoot } from "make-state";

const [useCommentId, commentIdState] = makeState(2922573); // 存入评论id
const [useComment] = makeState(async (get) => {
  const data = await fetch(
    `https://hacker-news.firebaseio.com/v0/item/${get(
      commentIdState
    )}.json?print=pretty`
  );
  return data.json();
}, null); 

const Post = () => {
  const [comment] = useComment();

  return (
    <div>
      <p>comment: {comment.text}</p>
      <p>by: {comment.by}</p>
    </div>
  );
};

const Demo = () => {
  return <Suspense fallback={<div>loading...</div>}>
    <Post />
  </Suspense>
}

借助Recoil提供的API,还可以支持非Suspense、并行串行请求等。

immer支持

makeState中的set等方法已经添加immer, 可以使用可变语法(CodeSandBox),immer将自动转为不可变变量。

const [useUser, userState] = makeState({name: 'Tom'})

function User () {
    const [user, setUser] = useUser()

    return <div>
        {user.name}
         <input onChange = {e => {
             // 支持可变语法
            setUser(user => {user.name = e.target.value})
        }}>
    </div>
}

可写驱动状态中的set同样支持可变语法(CodeSandBox)。

const [useUserName] = makeState(
  get => get(userState).name,
  (get, set, newValue) => {
    // 支持可变语法
    set(userState, user => {user.name = newValue})
  }
)

被immer包裹的变量被代理封装,给debug带来不便:

set(userState, user => {
  console.log(user) // ??? 火星文
  user.name = newValue}
)

可以用immer提供的current查看当前值, make-state导出此方法:

import makeState, {current} from 'make-state'

// 略..
set(userState, user => {
  console.log(current(user)) // 可以看懂user啦
  user.name = newValue}
)

从组件中传入参数

一些情况下,需要从组件向状态传入参数,此时可以用makeStates:

const [useStates] = makeStates(
  someProps =>   // 通过props获取组件传入的参数
    get =>  // 同makestate的参数
      someValue,
)

// 组件中:
const [data] = useStates(someProps) // 传入参数

假设有一个列表,每一项有一个文本框可以输入水果价格,不同的水果有不同的价格,向make-state传入水果的名称区分修改哪个水果的价格(CodeSandBox):

import { makeStates } from "make-state";

const [useFriute] = makeStates((name) => ({
  // 获取从组件中传入的水果名称
  name: name,
  price: 0.0
}));

const FriuteItem = ({ name }) => {
                                // 传入水果名称
  const [friute, setFriute] = useFriute(name);
  return (
    <div>
      {name} price:
      <input
        value={friute.price}
        onChange={(e) => {
          setFriute((friute) => {
            friute.price = e.target.value;
          });
        }}
      />
    </div>
  );
};

function Demo() {
  return (
    <div>
      <FriuteItem name="apple" />
      <FriuteItem name="banana" />
      <FriuteItem name="orange" />
    </div>
  );
}

驱动状态接收组件中传入参数

makeStates也可以用于驱动状态,此时需要套一层函数获取props,只读写法如下:

makeState(
  props =>          // 通过props获取组件传入的参数
    get => value,   // 同makestate的参数 
  null              // 表示只读
)

可写驱动状态:

makeState(
  // 读
  props =>         // 通过props获取组件传入的参数
    get =>   value,// 同makeState
      
  // 写
  props =>   // 通过props获取组件传入的参数
    (get, set, newValue) => {} // 同makeState
)

makeStates生成的状态,用在驱动状态中,同样需要传入props:

const [, state] = makeStates(name => name)
const [useDrived] = makeStates(
  props => get => get(state(props)) // 注意是:state(props)
)

将上面对的水果价格列表的例子优化,封装一共专门改水果价格的hook(CodeSandBox):

import { makeStates, RecoilRoot } from "make-state";

const [, friuteState] = makeStates((name) => ({
  // 获取从组件获取name
  name: name,
  price: 0.0
}));

const [useFriutePrice] = makeStates(
  (name) => (get) => get(friuteState(name)).price,
  (name) => (get, set, newValue) =>
    set(friuteState(name), (friute) => {
      friute.price = newValue;
    })
);

const FriuteItem = ({ name }) => {
  const [friutePrice, setFriutePrice] = useFriutePrice(name);
  console.log(friutePrice);
  return (
    <div>
      {name} price:
      <input
        value={friutePrice}
        onChange={(e) => {
          setFriutePrice(e.target.value);
        }}
      />
    </div>
  );
};

const Demo = () => {
  return (
    <div>
      <FriuteItem name="apple" />
      <FriuteItem name="banana" />
      <FriuteItem name="orange" />
    </div>
  );
}

通过makeState一样,makeStates的方法也被immer包装。 如果你使用过Recoil,会发现makeStates和atomFamily或者selectorFamily是一样的。

添加key

make-state会自动为每一个状态生成随机key,出于调试等目的,可以在makeState中传入最后一个字符串类型的参数,手动设置key,例如(CodeSandbox):

// 原始状态:
const [,count] = makeState(0, 'countKey')

// 只读驱动状态:
const [,double] = makeState(
    get => get(count),
    null
    'doubleKey' // 设key
)

// 可写驱动状态
const [,decrement] = makeState(
    get => get(count),
    (get, set, newValue) => {set(count, count => count - newValue)},
    'decrementKey' // 设key
)

如果你想知道当前状态的key,可以如下操作:

const [useCount, count, {key}] = makeState(0)

console.log(key) // 打印当前key

随机key每次运行都可能不一样,仅供调试使用,切忌用于业务。一个应用所有的命名key必须唯一,不然可能产生异常。

与Recoil共用

如果你熟悉Recoil,可以阅读此部分。 make-state只是对recoil的封装,没有魔法:

  • makeState创建原始状态:使用atom创建原子
  • makeState只读驱动状态:使用selector,并传入get参数
  • makeState可写状态:使用selector,并传入get和set参数

由于make-state生成的状态是RecoilState, 你可以将make-state生成的状态放入recoil中(CodeSandbox):

import makeState from 'make-state'
import {selector, useRecoilValue} from 'recoil'

const [,count] = makeState(1)

const doubleCount = selector({
    key: 'doubleCount',
    get: ({get}) => get(count) * 2
})

function Double () {
    const double = useRecoilValue(doubleCount)

    return <p>{double}</p>
}

同样, make-state中的get, set 方法与recoil一样,可以接收recoil产生的状态:

import makeState from 'make-state'
import {atom}  from 'recoil'

const count = atom({
    key: 'count',
    default: 0
})

const [useDouble] = makeState(
    get => get(count) * 2, null
)

function Double () {
    const [double] = useDouble()

    return <p>{double}</p>
}

make-staterecoil一样,支持异步,也可以使用watiAPI。 makeStates方法则类似atomFamilyselectorFamilymake-state同样支持recoil的devtool工具。

开发说明

本地开发

yarn start

基于snowpack的本地开发环境,用例见examples文件夹。

构建

目前只支持esm模式

yarn build
0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.0

3 years ago