Code Monkey home page Code Monkey logo

react-use-signal's Introduction

react-use-shared-state

react-use-shared-state为一个react hook工具,非常纯粹,只解决:

  • 数据共享
  • 防止非必要的re-render

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

使用

添加依赖:

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

使用:

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一起使用

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

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

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

获取引用值

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

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

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

订阅

可以对一个共享状态变化进行订阅:

const channel = useSharedState(1);
React.useEffect(() => {
  channel.subscribe((value) => {
    // 做一些额外的事情,比如有选择的更新当前某个当前组件状态的变化或者执行一些方法
  });
}, []);

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

计算属性

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

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

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包裹函数,降低非必要组件渲染。如:

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保持完美运行是一件非常困难的事情。如不应该传递给组件的属性值使用对象和函数的字面量。

如下面的对于组件的使用:

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包裹将没有一点意义。需要调整为:

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状态提升的状态一致即可。管理机制可以采取事件。

如:

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

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

react-use-signal's People

Contributors

joyerli avatar

Watchers

 avatar

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.