Giter Club home page Giter Club logo

icestore's Introduction

简体中文 | English

icestore

简单友好的状态管理方案。

NPM version build status NPM downloads codecov

版本

版本 代码分支 文档
V2 master Docs
V1 stable/1.x Docs

介绍

icestore 是面向 React 应用的、简单友好的状态管理方案。它包含以下核心特征:

  • 简单、熟悉的 API:不需要额外的学习成本,只需要了解 React Hooks,对 Redux 用户友好。
  • 集成异步处理:记录异步操作时的执行状态,简化视图中对于等待或错误的处理逻辑。
  • 支持组件 Class 写法:友好的兼容策略可以让老项目享受轻量状态管理的乐趣。
  • 良好的 TypeScript 支持:提供完整的 TypeScript 类型定义,在 VS Code 中能获得完整的类型检查和推断。

查看《能力对比表》了解更多细节。

文档

示例

快速开始

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, createModel } from '@ice/store';

const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time));

// 1️⃣ 使用模型定义你的状态
const counter = createModel({
  state: 0,
  reducers: {
    increment:(prevState) => prevState + 1,
    decrement:(prevState) => prevState - 1,
  },
  effects: () => ({
    async asyncDecrement() {
      await delay(1000);
      this.decrement();
    },
  })
});

const models = {
  counter,
};

// 2️⃣ 创建 Store
const store = createStore(models);


// 3️⃣ 消费模型
const { useModel } = store;
function Counter() {
  const [ count, dispatchers ] = useModel('counter');
  const { increment, asyncDecrement } = dispatchers;
  return (
    <div>
      <span>{count}</span>
      <button type="button" onClick={increment}>+</button>
      <button type="button" onClick={asyncDecrement}>-</button>
    </div>
  );
}

// 4️⃣ 绑定视图
const { Provider } = store;
function App() {
  return (
    <Provider>
      <Counter />
    </Provider>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

安装

使用 icestore 需要 React 在 16.8.0 版本以上。

npm install @ice/store --save

灵感

创造 icestore 的灵感来自于 rematchconstate

参与贡献

欢迎通过 issue 反馈问题。

开发:

$ cd icestore/
$ npm install
$ npm run test
$ npm run watch

$ cd examples/counter
$ npm install
$ npm link ../../                    # link icestore
$ npm link ../../node_modules/react  # link react
$ npm start

License

MIT

icestore's People

Contributors

alvinhui avatar beizhedenglong avatar imsobear avatar lucifer1004 avatar luhc228 avatar namepain avatar phobal avatar temper357 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

icestore's Issues

dispatch typings

How can I type annotate dispatch here?:
image
icestore doesnt export RootDispatch type.

[RFC] 更好的 effects 支持

需求

在状态管理中,A 数据更新依赖 B 数据的更新结果并需要对 B 数据进行异步操作。

Class Component 中的示例

https://codesandbox.io/s/rematch-count-demo-q7ld0?module=/ClassComponent.js

import React from "react";
import delay from "./delay";

export default class Component extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      todos: [
        {
          id: Date.now(),
          title: "react"
        }
      ],
      ids: [],
      evens: []
    };
  }

  async transformatTodosId(todos) {
    await delay(1000); // 模拟异步,异步中可能依赖 todos
    return todos.map((todo, index) => ({ ...todo, id: `${todo.id}_${index}` }));
  }

  async getEvens(ids) {
    await delay(1000); // 模拟异步,异步中可能依赖 ids
    return ids.map(id => id.split("_")[1]).filter(id => !(id % 2));
  }

  addTodo = todo => {
    this.setState(preState => ({
      todos: [...preState.todos, todo]
    }));
  };

  setIds = () => {
    this.setState(preState => ({
      ids: preState.todos.map(todo => todo.id)
    }));
  };

  setIdsByTodos = todos => {
    this.setState({
      ids: todos.map(todo => todo.id)
    });
  };

  setIdsByStateTodos = async () => {
    const todos = await this.transformatTodosId(this.state.todos);
    this.setIdsByTodos(todos);
  };

  setEvens = evens => {
    this.setState({ evens });
  };

  setEvensByStateIds = async () => {
    const evens = await this.getEvens(this.state.ids);
    this.setEvens(evens);
  };

  handleClick = async () => {
    // await this.addTodo({
    //   id: Date.now(),
    //   title: "alvin"
    // });
    // await this.setIdsByStateTodos();
    // await this.setEvensByStateIds();
    this.addTodo({
      id: Date.now(),
      title: "alvin"
    });
  };

  componentDidUpdate(prevProps, prevState) {
    if (prevState.todos.length !== this.state.todos.length) {
      this.setIdsByStateTodos();
    }

    if (prevState.ids.length !== this.state.ids.length) {
      this.setEvensByStateIds();
    }
  }

  render() {
    console.log("class component render...");
    return (
      <div>
        <span>{this.state.todos.length}</span>
        <br />
        <span>ids: {this.state.ids.join(",")}</span>
        <br />
        <span>evens: {this.state.evens.join(",")}</span>
        <br />
        <button onClick={this.handleClick}>click me</button>
      </div>
    );
  }
}

Function Component 中的示例

https://codesandbox.io/s/rematch-count-demo-q7ld0?module=/FunctionComponent.js

import React, { useState, useEffect } from "react";
import delay from "./delay";

export default function() {
  const [todos, setTodos] = useState([
    {
      id: Date.now(),
      title: "react"
    }
  ]);
  const [ids, setIds] = useState([]);
  const [evens, setEvens] = useState([]);

  async function transformatTodosId(todos) {
    await delay(1000); // 模拟异步,异步中可能依赖 todos
    return todos.map((todo, index) => ({ ...todo, id: `${todo.id}_${index}` }));
  }

  async function getEvens(ids) {
    await delay(1000); // 模拟异步,异步中可能依赖 ids
    return ids.map(id => id.split("_")[1]).filter(id => !(id % 2));
  }

  const addTodo = todo => {
    return setTodos(preState => [...preState, todo]);
  };

  const setIdsByTodos = data => {
    const ids = data.map(todo => todo.id);
    return setIds(ids);
  };

  function handleClick() {
    addTodo({
      id: Date.now(),
      title: "alvin"
    });
  }

  useEffect(() => {
    async function doSomeThing() {
      const newTodos = await transformatTodosId(todos);
      setIdsByTodos(newTodos);
    }

    doSomeThing();
  }, [todos]);

  useEffect(() => {
    async function doSomeThing() {
      const evens = await getEvens(ids);
      setEvens(evens);
    }

    doSomeThing();
  }, [ids]);

  console.log("function component render...");

  return (
    <div>
      <span>{todos.length}</span>
      <br />
      <span>ids: {ids.join(",")}</span>
      <br />
      <span>evens: {evens.join(",")}</span>
      <br />
      <button onClick={handleClick}>click me</button>
    </div>
  );
}

dva 中的示例

https://stackblitz.com/edit/dva-example-count-9eyczj?file=index.js

import React from 'react';
import dva, { connect } from 'dva';

const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout));

function *transformatTodosId(todos) {
  yield delay(1000); // 模拟异步,异步中可能依赖 todos
  return todos.map((todo, index) => ({ ...todo, id: `${todo.id}_${index}` }));
}


function *getEvens(ids) {
  yield delay(1000); // 模拟异步,异步中可能依赖 ids
  return ids.map(id => id.split("_")[1]).filter(id => !(id % 2));
}

const app = dva();

app.model({
  namespace: 'todos',
  state: [
    {
      id: Date.now(),
      title: "react"
    }
  ],
  reducers: {
    addTodo(preState, {payload: todo}) {
      return [...preState, todo];
    }
  },
  effects: {
    *addByAsync({payload}, { put, call, select }) {
      yield put({ type: 'addTodo', payload });

      const todos = yield select(state => state.todos);
      const nextTodos = yield call(transformatTodosId, todos);
      yield put({ type: 'ids/setByTodos', payload: nextTodos });

      const ids = yield select(state => state.ids);
      yield put({ type: 'evens/setEvensByIds', payload: ids });
    }
  },
});
app.model({
  namespace: 'ids',
  state: [],
  reducers: {
    setByTodos(preState, { payload: todos }) {
      return todos.map(todo => todo.id);
    }
  },
  effects: {
    *setByTodosAsync({ payload: todos }, { put, call }) {
      const nextTodos = yield call(transformatTodosId, todos);
      yield put({ type: 'setByTodos', payload: nextTodos });
    }
  }
});
app.model({
  namespace: 'evens',
  state: [],
  reducers: {
    update(preState, { payload }) {
      return payload;
    }
  },
  effects: {
    *setEvensByIds({ payload: ids }, { call, put }) {
      const evens = yield call(getEvens, ids);
      yield put({ type: 'update', payload: evens });
    }
  }
});

const App = connect(({ todos, ids, evens }) => ({
  todos, ids, evens
}))(function({ todos, ids, evens, dispatch }) {
  function handleClick() {
    dispatch({
      type: 'todos/addByAsync',
      payload: {
        id: Date.now(),
        title: "alvin"
      },
    });
  }
  return (
    <div>
      <span>{todos.length}</span>
      <br />
      <span>ids: {ids.join(",")}</span>
      <br />
      <span>evens: {evens.join(",")}</span>
      <br />
      <button onClick={handleClick}>click me</button>
    </div>
  );
});

app.router(() => <App />);

app.start('#root');

当前在 icestore 中如何实现?

https://codesandbox.io/s/cool-darkness-vqlhp

import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { createStore } from "@ice/store";

const delay = time => new Promise(resolve => setTimeout(() => resolve(), time));

async function transformatTodosId(todos) {
  await delay(1000); // 模拟异步,异步中可能依赖 todos
  return todos.map((todo, index) => ({ ...todo, id: `${todo.id}_${index}` }));
}

async function getEvens(ids) {
  await delay(1000); // 模拟异步,异步中可能依赖 ids
  return ids.map(id => id.split("_")[1]).filter(id => !(id % 2));
}

const todos = {
  state: [
    {
      id: Date.now(),
      title: "react"
    }
  ],
  reducers: {
    addTodo(preState, todo) {
      return [...preState, todo];
    }
  },
  effects: {
    async addByAsync(state, payload, actions) {
      actions.addTodo(payload);
    }
  }
};

const ids = {
  state: [],
  reducers: {
    setByTodos(preState, todos) {
      return todos.map(todo => todo.id);
    }
  },
  effects: {
    async setByTodosAsync(state, todos, actions) {
      const nextTodos = await transformatTodosId(todos);
      actions.setByTodos(nextTodos);
    }
  }
};

const evens = {
  state: [],
  reducers: {
    update(preState, nextState) {
      return nextState;
    }
  },
  effects: {
    async setEvensByIds(state, ids, actions) {
      const evens = await getEvens(ids);
      actions.update(evens);
    }
  }
};

const models = {
  todos,
  ids,
  evens
};

const store = createStore(models);

const { useModel } = store;
function Counter() {
  const [todos, actions] = useModel("todos");
  const [ids, idsActions] = useModel("ids");
  const [evens, evensActions] = useModel("evens");

  function handleClick() {
    actions.addByAsync({
      id: Date.now(),
      title: "alvin"
    });
  }

  useEffect(() => {
    idsActions.setByTodosAsync(todos);
  }, [todos]);

  useEffect(() => {
    evensActions.setEvensByIds(ids);
  }, [ids]);

  return (
    <div>
      <span>{todos.length}</span>
      <br />
      <span>ids: {ids.join(",")}</span>
      <br />
      <span>evens: {evens.join(",")}</span>
      <br />
      <button onClick={handleClick}>click me</button>
    </div>
  );
}

方案

参考 rematch:https://codesandbox.io/s/rematch-count-demo-b4rux?module=/models.js

import store from "./store";
const delay = time => new Promise(resolve => setTimeout(() => resolve(), time));

async function transformatTodosId(todos) {
  await delay(1000); // 模拟异步,异步中可能依赖 todos
  return todos.map((todo, index) => ({ ...todo, id: `${todo.id}_${index}` }));
}

async function getEvens(ids) {
  await delay(1000); // 模拟异步,异步中可能依赖 ids
  return ids.map(id => id.split("_")[1]).filter(id => !(id % 2));
}

export const todos = {
  state: [
    {
      id: Date.now(),
      title: "react"
    }
  ],
  reducers: {
    addTodo(preState, todo) {
      return [...preState, todo];
    }
  },
  // 提供返回的声明方式
  
  effects: dispatch => ({
    async addByAsync(payload, state) {
      dispatch.todos.addTodo(payload);

      const todos = store.getState().todos;
      await dispatch.ids.setByTodosAsync(todos);
      // const nextTodos = await transformatTodosId(todos);
      // dispatch.ids.setByTodos(nextTodos);

      const ids = store.getState().ids;
      await dispatch.evens.setEvensByIds(ids);
      // const evens = await getEvens(ids);
      // dispatch.evens.update(evens);
    }
  })
};

export const ids = {
  state: [],
  reducers: {
    setByTodos(preState, todos) {
      return todos.map(todo => todo.id);
    }
  },
  effects: dispatch => ({
    async setByTodosAsync(todos, state) {
      const nextTodos = await transformatTodosId(todos);
      this.setByTodos(nextTodos);
    }
  })
};

export const evens = {
  state: [],
  reducers: {
    update(preState, nextState) {
      return nextState;
    }
  },
  effects: dispatch => ({
    async setEvensByIds(ids, state) {
      const evens = await getEvens(ids);
      this.update(evens);
    }
  })
};

如何实现调用 effects 失败后弹一个 toast

Problem Description

// models/a.ts
export default {
  state: {},
  reducers: {},
  effects: (dispatch) => ({
    async test() { return Promise.reject(new Error('错误')); }
  }),
};

function Home() {
  const [state, dispatchers] = store.useModel('a');
  const effectsState = store.useModelEffectsState('a');

  useEffect(() => {
    dispatchers.test().then(() => {
      toast.success();
    }).catch(() => {
      toast.error();
    })
  }, [dispatchers]);
}

如上代码,因为 icestore 默认支持了 effectsState 能力,该能力会主动 try/catch effects 以拿到 error 状态直接提供给组件消费,因此调用端无法再对 effects 进行 catch,从合理性来讲这个实现是符合预期的。

try {
return await origEffect(...props);
} catch (error) {
// display error on console
console.error(error);
this.dispatch.error.show({ name, action }, error);
}

目前想要满足这个诉求的话,需要在 createStore() 的地方通过 disableError 的选项禁用内置 try/catch effect 的功能,但是这个方案影响面过大,会导致这个 store 里的所有 models 都没法使用内置提供的 error/loading 状态。

Proposed Solution

  1. useModel 支持 disableError/disableLoading 配置项(不确定是否能实现),这样当开发者需要主动 catch 的时候在对应的地方 disableError
    • 有个问题是 catch 的时候就不能用内置的 error 状态了,不过我感觉是合理的,这种情况想用就自己 setState
  2. useModel 支持 onError onSuccess 的配置项,也能满足诉求
  3. 类似 ahooks 3.0 单独提供一个 runAsync 方法,icestore 的场景感觉不太可取 alibaba/hooks#1173 (comment)

Additional Information

参考了 ahooks/useRequest 的实现,有一个 throwOnError 的选项,开启之后错误就会抛出并且不影响原先的 error 状态,不过方案整体也有一些 bug alibaba/hooks#812 ,ahooks 这里的实现比较复杂,虽然看起来比较强大,但不合理的地方也挺多。

useEffect中使用如何卸载?

示例代码:

const users = stores.useStore('users');

const { dataSource } = users;

useEffect(() => {
//页面切换时,这里会报错:index.js:2178 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
users.listUsers();
}, []);

[WIP]RFC: 2.0

目标

  1. 最小化内核,内置功能由插件承载,可选式载入。
    1. 内核只依赖 redux,useModel 相关的 API 由单个插件实现(@icestore/model
    2. error/loading/immer 功能不内置
  2. 移除兼容性代码,减少维护负担:convertEffects/convertActions/userActions... (代码中搜 @deprecated)
  3. API 统一和简化:store 和 withModel 的 API 使用方式一致,简化认知成本

API

createStore

最小内核,不依赖任何 UI 库。

import { createStore } from '@icestore/core';

const counter = {
  state: {
    value: 0,
  },
  reducers: {
    add: (state, payload) => {
      state.value = state.value + payload;
    },
  },
};

const store = createStore({ counter });

// dispatch reducers actions
store.dispatch({ type: 'counter/increment', payload: 1 }) // regular dispatch usage
store.dispatch.counter.increment(1) // the same as above but with action dispatcher
store.getState() // { counter: 2 }

// dispatch effects actions
store.dispatch({ type: 'counter/incrementAsync', payload: 1 }) // regular dispatch usage
store.dispatch.counter.incrementAsync(1) // the same as above but with action dispatcher

Model Plugin

与 React 相关的 API 绑定放到 @icestore/model

import { createStore } from '@icestore/core';
import createModelPlugin from '@icestore/model';
import * as reactRedux from 'react-redux';
import * as models from './models';

const { 
  Provider,
  getModel,
  withModel,
} = createStore(models, { plugins: [createModelPlugin(reactRedux)] });

getModel

import store from '@/store';
const counter = store.getModel('counter');
// counter.useValue()
//     counter.useState()
//     counter.useDispatchers()
// counter.getValue()
//     counter.getState()
//     counter.getDispatchers()
// counter.withValue()
//     counter.withDispatchers()

function FunctionComponent() {
  const [ state, dispatchers ] = counter.useValue();

  state.value; // 0
  dispatchers.add(1); // state.value === 1
}
withValue

使用该 API 将模型绑定到 Class 组件上。

import { ExtractIModelFromModelConfig } from '@icestore/core';
import todosModel from '@/models/todos';
import store from '@/store';

const counter = store.getModel('counter');

interface Props {
  todos: ExtractIModelFromModelConfig<typeof todosModel>; // `withModel` automatically adds the name of the model as the property
}

class TodoList extends Component<Props> {
  render() {
    const { counter } = this.props;
    const [ state, dispatchers ] = counter;
    
    state.value; // 0
    dispatchers.add(1);
  }
} 

export default counter.withValue()(TodoList);

withModel

该方法用于在组件中快速使用 Model。

import { withModel } from '@icestore/core';
import model from './model';

function Todos({ model }) {
  const {
    useValue,
    useState,
    useDispatchers,
    getValue,
    getState,
    getDispatchers,
  } = model;
  const [ state, dispatchers ] = useValue();

  state.value; // 0
  dispatchers.add(1); // state.value === 1
}

export default withModel(model)(Todos);

如何升级

createStore

1.x

import { createStore } from '@ice/store';
import * as models from './models';

const store = createStore(models);

2.x

import { createStore } from '@icestore/core';
import createModelPlugin from '@icestore/model';
import createErrorPlugin from '@icestore/error';
import createLoadingPlugin from '@icestore/loading';
import createImmerPlugin from '@icestore/immer';
import * as models from './models';

const store = createStore(
  models,
  { 
    plugins: [
      createModelPlugin(reactRedux),
      createErrorPlugin(),
      createLoadingPlugin(),
      createImmerPlugin(),
    ] 
  }
);

useModel

1.x

import store from '@/store';

const { useModel } = store;

function FunctionComponent() {
  const [ state, dispatchers ] = useModel('counter');

  state.value; // 0
  dispatchers.add(1); // state.value === 1
}

2.x

import store from '@/store';

const counter = store.getModel('counter');

function FunctionComponent() {
  const [ state, dispatchers ] = counter.useValue();

  state.value; // 0
  dispatchers.add(1); // state.value === 1
}

deprecated

舍弃的 APIs:

  • EffectsState
    • useModelEffectsState
    • withModelEffectsState
  • Actions
    • useActions
    • withModelActions

所有低于 1.4.x 的 API 都不会兼容。

项目目录结构

.
├── LICENSE
├── README.md
├── README.zh-CN.md
├── codecov.yml
├── commitlint.config.js
├── docs
├── examples
├── packages
│   ├── core
│   │   ├── package.json
│   │   └── src
│   │       └── index.ts
│   ├── error
│   │   ├── package.json
│   │   └── src
│   │       └── index.tsx
│   ├── immer
│   │   ├── package.json
│   │   └── src
│   │       └── index.ts
│   ├── loading
│   │   ├── package.json
│   │   └── src
│   │       └── index.tsx
│   └── model
│       ├── package.json
│       └── src
│           └── index.tsx
└── tsconfig.json

How can I invoke an effect or another reducer from a reducer?

const counter = {
  state: 0,
  reducers: {
    decide_what_to_do:(prevState) => {
      if (prevState > 10) this.asyncDecrement() // Cannot read property 'asyncDecrement' of undefined
      else this.increment(); // Cannot read property 'increment' of undefined
    },
    decrement:(prevState) => prevState - 1,
    increment:(prevState) => prevState + 1,
  },
  effects: () => ({
    async asyncDecrement() {
      await delay(1000);
      this.decrement(); 
    },
  }),
};

Feature: support custom selector

目前 useModel 只支持传入单个字符获取一个 model 的 state。
希望能够渗入获得具体 model 的 state 的特定值。

比如:

const nestedCounter = {
  state: {
    counter: 0,
    otherCount: 0
  },
  reducers: {
    incrementNested: prevState => {
      prevState.counter += 1;
    },
    incrementOther: prevState => {
      prevState.otherCount += 1;
    }
  }
};

如果组件A 只是调用 incrementNested 方法,而组件B只是依赖了 otherCount 的数据。组件B 依然会触发 re-render。(当然可以通过 return 的时候 useMemo() 来解决这个问题)

如果 useModel 的 selector 可以暴露给用户,或者我们提供自定义的对象取值,会不会更香?

比如:

const [ counter]  = useModel('nestedCounter.counter')
const [otherCount] = useModel('nestedCounter.othterCount');

或者

const [counter] = useModel(state => state.nestedCounter.counter)

https://codesandbox.io/s/brave-fire-jnjoy?file=/src/index.tsx

Bug: 文档错误

文档 https://github.com/ice-lab/icestore/blob/master/docs/api.zh-CN.md
const todos = {
state: [
{
title: 'Learn typescript',
done: true,
},
],
reducers: {
// 正确用法
add(state, todo) {
state.push(todo);
},
// 错误用法
add(state, title, done) {
state.push({ title, done });
},
},
};

// 使用时:
function Component() {
const { add } = store.useModelDispathers('todos');
function handleClick () {
add({ title: 'Learn React', done: false }); // 正确用法
add('Learn React', false); // 错误用法
}
}

这里的state 才进行了代码,而对payload 解构是没有影响的

[RFC] 获取最新状态

需求

原始需求来自于 @亦森 :

https://codesandbox.io/s/gallant-fast-w8klw
第三十八行代码期望获取到最新的 state。

在开发中会有这类需求。例如:

import { useState, useMemo } from "React";	
import { getModelState } from "./store";	

function Logger({ foo }) {	
  // case 1 在闭包中获取最新状态(能力欠缺,期望有 getModelState 这样的 API)
  const doOhterThing = useCallback(
    (payload) => {
      const counter = getModelState('counter');	
      alert(counter + foo);
    },
    [foo]
  );

  // case 2 只使用状态而不订阅更新(性能优化,期望有 getModelState 这样的 API)
  function doSomeThing() {	
    const counter = getModelState('counter');	
    alert(counter);
  };

  
  return (
    <div>
      <button onClick={doSomeThing}>click 1<button>
      <button onClick={doOhterThing}>click 2<button>
    </div>
  );
}	 

方案

参考 redux ,提供 getState API:

import { useState, useMemo } from "React";	
import store from "./store";	

function Logger({ foo }) {	
  // case 1 在闭包中获取最新状态(能力)
  const doOhterThing = useCallback(
    (payload) => {
      const counter = store.getState().counter;	
      alert(counter + foo);
    },
    [foo]
  );

  // case 2 只使用状态而不订阅更新(性能优化的手段)
  function doSomeThing() {	
    const counter = store.getState().counter;	
    alert(counter);
  };

  
  return (
    <div>
      <button onClick={doSomeThing}>click 1<button>
      <button onClick={doOhterThing}>click 2<button>
    </div>
  );
}	 

Feature: 如何获取计算属性

 state: {
    hasChanged: false,
    get isChangeed() {
      return Number(this.hasChanged);
    },
  },

这种情况获取isChangeed总是为0,hasChanged变更后isChangeed不会改变

Bug: ModelEffects 或 createModel 类型定义错误

@ice/store version: 2.0.3

Steps To Reproduce

  1. 下载 @ice/store 源码
  2. 进入 example/todos 文件夹
  3. npm i
  4. 修改 src/models/todos.ts 文件的 effects,查看第二个参数的类型

通读了 docs 文件夹下的所有文档,没有找到解决办法。我个人定位的原因是 ModelEffects 类型定义错误导致。

Link to code example:

The current behavior

image
image

The expected behavior

定义effects时能正确解析出第二个参数的类型

Icestore和Field如何集合使用

Field进行new的时候,必须传入类型为React.Component的this参数,但是使用icestore的情况下,是使用的函数组件的方式,这个时候应该如何使用?

react version error

Issue is uniformly managed in alibaba/ice. The issues not in alibaba/ice will be closed directly.

Issue 统一在 alibaba/ice 仓库管理,不在 alibaba/ice 的 issue 会被直接关闭。

image

这里的 peerDependencies 中依赖的 react 版本应该是 ^16.8.0 吧?

子组件会多 render 一次的问题

在线示例:https://codesandbox.io/s/green-star-f19k0

如果把源码中 private setState() 中的遍历触发 forceupdate 放在 unstable_batchedUpdates 中执行,可以避免同步 action 触发时候 ,子组件重新 render 。

但是 ,action 还可以返回一个 promise ,这样的话如果需要 action 的 loading 状态,就会连续两次触发 forceupdate ,总是会触发子组件 render 两次的。

RFC: effects 中的 dispatch 和 rootState 类型提示支持

Problem Description

当前在 effects 中 使用 dispatchrootState 时并没有完整的类型提示, 有一种方式是通过as unknown as xx 的形式来断言,但这样在每次使用的时候都需要写一遍断言,体验相当不好。

Proposed Solution

在 @ice/store 先声明两个 interface RootStateRootDisaptch

image

然后在实际项目中通过declare module '@ice/store' 的方式来扩大这两个类型

image

这样操作之后就可以得到完整的类型提示了

image
image

但是这种方式的缺点就是需要自己把每个 model 都写一遍。尝试着用 models 的方式写,会报【以递归方式将自身引用为基类】的错误

image

当然每个 model 都写一遍声明,这种没啥意义的重复劳动可以在 ice 或者 rax 的 store 插件中通过工程化的方式来自动生成,做到用户无感。

Bug: 类型推导问题

@ice/store version: 1.1.0

Steps To Reproduce

interface IState {
  count: number;
};

const counter = {
  state: {
    count: 20,
  },
  reducers: {
    addCount(prevState: IState, count: number): IState {
      return {
        ...prevState,
        count: prevState.count += count,
      };
    },
  },
  effects: {
    async addCountAsync(prevState: IState, count: number, actions): Promise<void> {
      await new Promise((resolve) => setTimeout(resolve, 0.5 * 1000));
      actions.addCount(count);
    }
  }
}

export default counter;

Link to code example:

https://github.com/imsobear/icejs-store-demo

The current behavior

useModel:

image

useModelActions:

image

useModelEffectsState:

image

The expected behavior

You know.

icestore和@alifd/next的table一起使用时,如果设置了isTree会报错

icestore和@alifd/next的table一起使用时,如果设置了isTree会报错
TypeError: 'set' on proxy: trap returned falsish for property '__level'
at tree.js:103
at Proxy.forEach ()
at loop (tree.js:102)
at TreeTable.normalizeDataSource (tree.js:110)
at TreeTable.render (tree.js:154)
at finishClassComponent (react-dom.development.js:14741)
at updateClassComponent (react-dom.development.js:14696)
at beginWork (react-dom.development.js:15644)
at performUnitOfWork (react-dom.development.js:19312)
at workLoop (react-dom.development.js:19352)
at renderRoot (react-dom.development.js:19435)
at performWorkOnRoot (react-dom.development.js:20342)
at performWork (react-dom.development.js:20254)
at performSyncWork (react-dom.development.js:20228)
at requestWork (react-dom.development.js:20097)
at scheduleWork (react-dom.development.js:19911)
at dispatchAction (react-dom.development.js:13599)
at store.js:165
at Array.forEach ()
at Store../node_modules/@ice/store/lib/store.js.Store.setState (store.js:165)
at afterExec (store.js:120)
at Store. (store.js:130)
at step (store.js:43)
at Object.next (store.js:24)
at fulfilled (store.js:15)

Feature: 提供 1.x.0 到 1.3.x 的升级工具

问题描述

icestore 从 1.0 到 1.3 有不少的 API 变更,这些变更虽然向下兼容,但是为了能够让更多的开发者使用新版本,期望提供一个用于升级老项目的命令行工具。

期望的方案

$ icestore --upgrade

该工具将主要应用于 icejs 项目:

  • 扫描项目的 model 文件,并判断其 API 使用方式,如果是老版本则将代码更新为最新的 API 使用方式;
  • 扫描项目的 React 组件,并判断其 API 使用方式,如果是老版本则将代码更新为最新的 API 使用方式;

建议

Classic redux dispatch?

I'd like to use classic redux approach where action is passed to dispatch. Is it possible?
Without specifying in event handler which particular reducer should be invoked.
I can see that there is a dispatch arg in effects fn and also a vague notion of actions in type definitions. But I didn't see anything particular in the api docs.
Do I always have to specify which particular reducer I want to use? What if my action requires several reducers invoked simultaneously?

Bug: 在 umi 中使用 icestore

@ice/store version:

Steps To Reproduce

  1. 按照文档demo操作
const counter = {
  state: 0,
  reducers: {
    increment: (prevState) => prevState + 1,
    decrement: (prevState) => prevState - 1,
  },
  effects: () => ({
    async asyncDecrement() {
      await delay(1000);
      this.decrement();
    },
  }),
};

const { useModel } = createStore({
  counter,
});

function FunctionComponent() {
  const [state, dispatchers] = useModel('counter');

  return <p>{state}</p>;
}

Link to code example:

The current behavior

TypeError: Cannot read property 'store' of null
useSelector
./node_modules/@ice/store/node_modules/react-redux/es/hooks/useSelector.js:126:33
  123 | }
  124 | 
  125 | var _useReduxContext = useReduxContext(),
> 126 |     store = _useReduxContext.store,
      |                             ^  127 |     contextSub = _useReduxContext.subscription;
  128 | 
  129 | var selectedState = useSelectorWithStoreAndSubscription(selector, equalityFn, store, contextSub);

useModel('counter');

执行这里报错 版本是 1.4.3

在new Icestore() 出现错误,

版本
"@ice/store": "^1.0.0",
"@ice/store-logger": "^0.1.0",
"react": "^16.9.0",
——————
当按照你们的demo去创建store的时候出现在这一步
const icestore = new Icestore();

TypeError: _ice_store__WEBPACK_IMPORTED_MODULE_0___default.a is not a constructor
Module../src/stores/index.js
src/stores/index.js:5
2 | import logger from '@ice/store-logger';
3 | import home from './home/home';
4 |

5 | const icestore = new Icestore();
6 | const middlewares = [];
7 | if (process.env.NODE_ENV !== 'production') {
8 | middlewares.push(logger);

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.