npm package: @joyer/react-use-shared-state
Description: 一个超简单的跨组件通信的共享状态库
Author: joyerli
Latest version: 1.0.1

# react-use-shared-state
 
react-use-shared-state为一个react hook工具，非常纯粹，只解决：
* 数据共享
* 防止非必要的re-render

它只能在react hooks中使用，可以让你像使用`useState`一样管理跨组件的状态。支持细颗粒度，直接面向基本数据类型，没有额外的数据存储中心(直接利用react的状态)。也不依赖`React.memo`才能发挥作用。

## 使用

添加依赖:

```shell
yarn add @joyer/react-use-shared-state
```

使用：
```jsx
import useSharedState from '@joyer/react-use-shared-state';

const Context = React.createContext({});
const ChildA: React.FC = () => {
  const { numberChannel } = React.useContext(Context);
  const number = numberChannel.useValue();
  return (<div>{number}</div>);
};
const ChildB: React.FC<{}> = () => {
  const { numberChannel } = React.useContext(Context);
  return (<button onClick={() => {
    numberChannel.setValue(10);
  }}>按钮</button>);
};
const Root: React.FC<{}> = () => {
  const numberChannel = useSharedState(1);
  return (<Context.Provider value={{ numberChannel }}>
    <ChildA />
    <ChildB />
  </Context.Provider>);
};
```
> 你可以配合context流行的工具库unstated-next一起使用

使用`useSharedState`hook声明创建一个跨组件共享状态的通道，该通道返回值引用固定。创建通道时并没有状态的生成，也就是说，声明通道的组件并不会受到对应状态更新而导致当前组件的re-render。调用通道`sharedState`返回值的`useValue`hook时才会在当前组件中创建一个状态，该状态会受到通道中事件流的控制从而触发当前组件的re-render。具体内容如果感兴趣可以阅读后续的背景和实现原理。

在上文的案例中，你会发现，当点击按钮时，只有组件`ChildA`会发生`re-render`，真正做到「应渲尽渲」,而不需要渲染的，一个都不会，且这一切还不需要依赖`React.memo`。

> 下文中会介绍一些其他的api, 这些api其实是借鉴了signal的理念中的设计。

### 获取引用值
如果需要实时获取一个共享状态的最新值(事件中的命令式使用，本质为`ref`)，可以直接调用共享状态通道的`getValue`函数获取：

```tsx
const channel = useSharedState(1);
const value = channel.getValue();
```

这样返回的`value`将不会是一个响应式数据(为一个`ref`值)，在状态被其他组件更新后，也不会导致当前组件的re-render。建议在当前组件中只有事件逻辑中需要一个共享组件的的值时使用，可以避免当前组件因为该状态变化触发re-render。

### 订阅
可以对一个共享状态变化进行订阅：
```tsx
const channel = useSharedState(1);
React.useEffect(() => {
  channel.subscribe((value) => {
    // 做一些额外的事情，比如有选择的更新当前某个当前组件状态的变化或者执行一些方法
  });
}, []);
```

使用订阅可以只在特定的条件下才去触发当前组件的一些行为，避免使用一个状态的完全响应式能力，从而手动降低组件一些非必要的re-render。

### 计算属性
如上文中所说，使用`useSharedState`时，并不是像`Context`那样进行状态提升，只是声明了一个共享状态管理通道，在提供通道的共同祖节点中，是无法对状态进行`useMemo`来生成计算属性。

如果需要使用类似计算属性的能力，需要理解：由于状态并没有提升，这些状态还是是分散在各个组件中的，只是通过一个相同的通道进行统一管理而已。对于需要复用多个状态处理的逻辑，可以封装成新的hooks，如：

```tsx
import useSharedState from '@joyer/react-use-shared-state';

const Context = React.createContext({})

function useIsAdult() {
  const { ageChannel } = React.useContext(Context);
  const age = ageChannel.useValue();
  const isAdult = React.useMemo(() => {
    return age >= 18;
  }, [age]);
}

const ChildA: React.FC = () => {
  const isAdult = useIsAdult();
  return (<div>
    {isAdult ? '成年人' : '未成年'}
  </div>);
};
const ChildB: React.FC<{}> = () => {
  const { ageChannel } = React.useContext(Context);
  return (<button onClick={() => {
    ageChannel.setValue(10);
  }}>按钮</button>);
};
const Root: React.FC<{}> = () => {
  const ageChannel = useSharedState(1);
  return (<Context.Provider value={{ ageChannel }}>
    <ChildA />
    <ChildB />
  </Context.Provider>);
};
```

## 优势

1. 非常轻量，可以从下文中的背景和设计理念来看，react-use-shared-state想要解决的问题非常简单，本质上就是一个事件流工具；

2. 由于轻量，所以灵活。

3. 不依赖react.memo，连equals计算消耗都没有；

4. 保持跟useState同样的颗粒度。当你不需要redux，mobx这种基于对象的状态流，不喜欢抽象什么领域，模型的情况下，使用react-use-shared-state体验非常友好，使用体验也非常接近于原生的hook；

5. 性能卓越，非常容易做到「真正需要渲染的地方才渲染」的效果；

6. 非常容易集成到已有系统。就算接手的系统已经是一座「屎山」，使用react-use-shared-state进行改造也非常简单(只需要对跨组件的状态进行一一改造就行)，还可以渐进式慢慢调整。对于不考虑后续可维护性和可读性的话，可以简单的将一个页面的跨组件状态都放在同一个地方，且这种行为不会影响性能。

## 背景

如果你也喜欢使用react的函数组件，并喜欢使用react原生的hook进行状态管理，不想引入redux,MboX这种具有自己独立的状态管理的重量级/对象级的状态流框架的话，可以使用当前工具。

首先探讨如果不采用redux,mobx，使用原生的react的跨组件共享状态方案`Context`，会具备那些问题？

react原生的跨组件通信为`Context`。在使用`Context`进行组件之间通信时，需要进行状态提升，提升到需要通信的组件的公共的祖先节点之中。这会导致当数据的变化时祖先节点产生`re-render`, 从而祖先节点中的整个组件树都会re-render，带来非常大的性能损失。react官方推荐使用`React.memo`包裹函数，降低非必要组件渲染。如：

```tsx
const Context = React.createContext<any>({})
const SubCompA: React.FC<{}> = React.memo(() => {
  console.log('渲染了A');
  const { number } = React.useContext(Context);
  return (<div>
    {number}
  </div>);
});
const SubCompC: React.FC<{}> = React.memo(() => {
  console.log('渲染了C');
  const { setNumber } = React.useContext(Context);
  return (<button className='__button' onClick={() => {
    setNumber(10);
  }}>我是按钮</button>);
});
const SubCompB: React.FC<{}> = React.memo(() => {
  console.log('渲染了B');
  return (<div>
    <SubCompC />
  </div>);
});
const SubCompD: React.FC<{}> = React.memo(() => {
  console.log('渲染了D');
  return (<div></div>);
});
const Root: React.FC<{}> = React.memo(() => {
  console.log('渲染了Root');
  const [number, setNumber] = React.useState(1);
  return (<Context.Provider value={{ number, setNumber }}>
    <SubCompA />
    <SubCompB />
    <SubCompD />
  </Context.Provider>);
});
```

在本案例中，点击按钮后，会导致组件`SubCompA`, `SubCompC`, `Root`组件re-render，但`SubCompC`, `Root`都是不受期望的re-render。且在实际使用情况下，性能会损失更大，因为：
* 不会把每一个状态单独放到一个的Context中。当Context中包含多个状态时，任何一个状态发生变化后，不管有没有依赖具体发生变化的那个状态，所有使用了该Context的组件都会更新，导致re-render的非法扩散(不受期望的re-render)。
* 非常依靠`React.memo`发挥效果，但在实际开发过程，使`React.memo`保持完美运行是一件非常困难的事情。如不应该传递给组件的属性值使用对象和函数的字面量。


如下面的对于组件的使用：
```tsx
const CompA: React.FC<{}> = React.memo(() => {
  return (<div>1</div>);
});

const Root: React.FC<{}> = React.memo(() => {
  return (<CompA objectProp={{ name: 'joy' }} onClick={() => {
    // ....
  }} />);
});
```
在本案例中，上文对于`CompA`进行`React.memo`包裹将没有一点意义。需要调整为：

```tsx
const CompA: React.FC<{}> = React.memo(() => {
  return (<div>1</div>);
});

const Root: React.FC<{}> = React.memo(() => {
  const objectProp = React.useMemo(() => ({ name: 'joy' }));
  const handleClick = React.useCallback(() => {
    // ....
  }, []);
  return (<CompA objectProp={objectProp} onClick={handleClick} />);
});
```

> 这里并不是想说`memo`没有必要。`memo`是提升性能的一个很重要的手段，在平常开发过程中，非常需要严格遵循，努力使`memo`发挥作用。

综上所述，`Context`中的性能损失，主要的原因是状态提升导致更大范围的组件re-render造成。

## 设计理念

为了解决原生Context的问题，不能进行状态进行提升，而是在不同的组件中存在多个相同含义的状态，然后通过统一的机制管理这些状态的值，使它实际效果跟Context状态提升的状态一致即可。管理机制可以采取事件。

如：
```tsx
const eventEmitter = new EventEmitter();
const CompA: React.FC<{}> = React.memo(() => {
  const [age, setAge] = React.useState(0);
  React.useEffect(() => {
    eventEmitter.addListener('updateAge', setAge);
  }, []);
  return (<div>{state}</div>);
});

const CompB: React.FC<{}> = React.memo(() => {
  return (<div onClick={() => {
    eventEmitter.emit('updateAge', 10);
  }}>1</div>);
});

const Root: React.FC<{}> = React.memo(() => {
  return (<>
    <CompA />
    <CompB />
  </>);
});
```

但在复杂系统中，需要的管理的状态流非常庞大，事件也将非常多以至于难以管理，此时需要将其封装，屏蔽其复杂性。

在react-use-shared-state中，使用一个事件器进行通信。对于一组使用同样事件名的状态称为通道，该通道会在hook`useSharedState`调用时创建，此时主要是生成一个随机的事件名(该事件名在调用hook的组件的声明周期内保持不变)，同时在组件销毁时，自动注销事件的监听器。实际的状态生成和注册事件的逻辑，在需要状态的组件中调用执行，也就是对`useSharedState`返回的通道中的`useValue`hook调用时才执行创建状态和注册监听事件修改状态的逻辑。

> `useSharedState`这个hook返回值中有一个hook, 可以理解它为一个hook工厂。这在react官方中是不推荐的，但在实际使用过程中，并不会导致某个组件hook的数量处于动态变化的情况下。