New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@ivliu/react-signal

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ivliu/react-signal

Signal(信号)是一种存储应用状态的形式,类似于 React 中的 useState()。但是,有一些关键性差异使 Signal 更具优势。Vue、Preact、Solid 和 Qwik 等流行 JavaScript 框架都支持 Signal。

latest
Source
npmnpm
Version
1.0.8
Version published
Weekly downloads
0
-100%
Maintainers
1
Weekly downloads
 
Created
Source

@ivliu/react-signal

Signal(信号)是一种存储应用状态的形式,类似于 React 中的 useState()。但是,有一些关键性差异使 Signal 更具优势。Vue、Preact、Solid 和 Qwik 等流行 JavaScript 框架都支持 Signal。

那么react结合signal能产生什么样的火花,能解决什么问题呢?

Signal 是什么?

Signal 和 State 之间的主要区别在于 Signal 返回一个 getter 和一个 setter,而非响应式系统返回一个值和一个 setter。

useState() = value + setter
useSignal() = getter + setter

注意:有些响应式系统同时返回一个 getter/setter,有些则返回两个单独的引用,但思想是一样的。

我们拿solidjs举个例子,因为react-signal的api设计和solidjs保持一致

const Counter = () => {
  const [count, setCount] = createSignal(0);

  return (
    <button onClick={() => setCount(count() + 1)}>{count}</button>
  )
}

安装

using pnpm

pnpm add @ivliu/react-signal

using yarn

yarn add @ivliu/react-signal

using npm

npm install @ivliu/react-signal --save

用法

import { useSignal, useEffect, untrack } from '@ivliu/react-signal';

const App = () => {
  // ? [getter, setter]
  const [count, setCount] = useSignal(60);
  // ? untrack count();
  useEffect(() => {
    setInterval(() => {
      setCount(untrack(() => count()) - 1);
    }, 1000);
  });
  // ? auto track count();
  useEffect(() => {
    console.log('effect', count());
    return () => console.log('destroy', count());
  });
  // ? useEffect with undefined deps
  useEffect(() => {
    console.log('update');
  }, null);

  return <div>{count()}</div>;
};

调试

# 安装依赖
pnpm install
# 运行
npm start
# 进入example
cd example
# 安装依赖
pnpm install # or yarn
# 运行
npm start

打开http://localhost:1234,即可查看,也可更改example/index.tsx来体验

react hooks的问题

提起react hooks,我们作为开发者可以说是又爱又恨,爱的是它可以让函数组件拥有类组件的功能,从而更方便地管理组件状态,同时在逻辑复用上相较于HOC或者render props更简单更轻量。恨的是它带来了一些心智负担,尤其是闭包和显式依赖问题。

react-signal在一定程度上可以解决这些问题

API

react-signal使用useSignal代替useState,返回了getter和setter。

为了实现依赖自动追踪,我们重写了useEffect、useLayoutEffect、useInsertionEffect、useMemo、useCallback,且命名与react保持一致。

另外我们还提供了一些高级api,createSignal、untrack、destroy。

下面将会详细介绍每一个api。

useSignal

useSignal用于替换useState,它返回一个getter和setter。

import { useSignal, useEffect } from '@ivliu/react-signal';

function App() {
  const [count, setCount] = useSignal(0);

  useEffect(() => {
    const handle = setTimeout(() => { 
      // 输出最新值10,而非初次访问的闭包值
      console.log(count()) 
    }, 1000);
    return () => clearTimeout(handle);
  })
  // useEffect都不需要写依赖了
  useEffect(() => {
    setCount(10);
  })

  // 取值改为getter方式
  return <div>{count()}</div>
}

如果signal初值初始化成本较高,那么你可以通过函数指定。

// new person仅会初始化一次
useSignal(() => new Person())

另外还可以用createSignal创建初始值,但是注意createSignal需要声明在组件外部。

import { createSignal, useSignal, useEffect } from '@ivliu/react-signal';

const externalSignal = createSignal(0);

function App() {
  const [count, setCount] = useSignal(externalSignal);

  useEffect(() => {
    const handle = setTimeout(() => { 
      // 输出最新值10,而非初次访问的闭包值
      console.log(count()) 
    }, 1000);
    return () => clearTimeout(handle);
  })
  // useEffect都不需要写依赖了
  useEffect(() => {
    setCount(10);
  })

  // 取值改为getter方式
  return <div>{count()}</div>
}

useReducer

import { useReducer, useEffect } from '@ivliu/react-signal';

function App() {
  const [count, dispatch] = useReducer((prevValue) => prevValue + 1, 0);

  // dispatch引用是稳定的,当需要对子组件缓存时很有效果
  return <div onClick={dispatch}>{count()}</div>
}

useEffect

useEffect用于替换native useEffect,默认不需要填写依赖。执行时机和react effect一致

useEffect(() => {
  /** count()会自动跟踪,count()发生变化时,effect函数会重新执行 */
  console.log(count())
})

如果想实现等效native Effect不传依赖,即useEffect回调每次渲染都重新执行的效果的话,则依赖项需要显式传入null。

useEffect(() => {
  console.log(count())
}, null)

useLayoutEffect、useInsertionEffect同理。

useCallback

const onClick = useCallback(() => {
  console.log(count());
})

如果函数仅仅依赖signal的话,那么想实现一个引用稳定的函数将轻而易举,这是个附加的feature。

useMemo

function App() {
  const [count, setCount] = useSignal(0);

  const doubleCount = useMemo(() => {
    return count() * 2;
  });

  return <div onClick={() => setCount(count() + 1)}>{doubleCount()}</div>
}

createSignal

createSignal是脱离react组件创建signal的方式,本意是为了和useSyncExternalStore更好的结合使用。

结合useSyncExternalStore

import { useSyncExternalStore } from 'react';
import { createSignal, useCallback } from '@ivliu/react-signal';

const store = createSignal({ theme: 'light' });

function App() {
  const { theme } = useSyncExternalStore(
    store.subscribe,
    useCallback(() => store.value),
  );
  
  return <div onClick={() => store.value = { theme: 'dark' } }>{theme}</div>
}

结合useSignal

import { createSignal, useSignal, useEffect } from '@ivliu/react-signal';

const externalSignal = createSignal(0);

externalSignal.subscribe((value) => console.log(value));

function App() {
  const [count, setCount] = useSignal(externalSignal);

  useEffect(() => {
    const handle = setTimeout(() => { 
      // 输出最新值10,而非初次访问的闭包值
      console.log(count()) 
    }, 1000);
    return () => clearTimeout(handle);
  })
  // useEffect都不需要写依赖了
  useEffect(() => {
    setCount(10);
  })

  // 取值改为getter方式
  return <div>{count()}</div>
}

同时我们可以用它做一些状态保持,比如最常见的页码保持。 我们有一个列表页,然后在某页进入详情,然后返回,我们肯定希望保持在对应页,利用createSignal就可以轻松实现,因为组件销毁的时候,状态仍然保持在内存里,组件再次挂载时访问的是缓存状态。

注意不要一个external signal供多个useSignal使用。

untrack

我们实现了effect依赖的自动追踪,那么我们不想追踪某些变量的话,我们可以用untrack包裹

useEffect(() => {
  // 此时count()不会追踪,setInterval仅会设置一次
  const handle = setInterval(() => {
    setCount(untrack(() => count()) - 1);
  }, 1000);
  return () => clearInterval(handle);
});

destroy

先看个问题

function App() {
  const [count, setCount] = useState(0);
  const [person, setPerson] = useState({ name: '' });

  const countRef = useRef(count);

  countRef.current = count;

  useEffect(() => {
    // ? person.name每次更新,两次输出的值是否一致
    console.log(countRef.current);
    return () => console.log(countRef.current);
  }, [person.name]);

  return <input value={person.name} onChange={(e) => {
    setPerson({ name: e.target.name });
  }} />
}

揭晓答案,不一致。因为effect destroy函数是在下一次渲染执行的。

因为我们提供了destroy api,它用在native useEffect内部访问signal的情况。

// ! native useEffect
useEffect(() => {
  // ? person.name每次更新,两次输出的值保持一致
  console.log(count());
  return destroy(() => console.log(count()))
}, [person.name]);

渐进接入

react-signal并非脱离react创造新概念,且和细粒度更新没什么关系,它仅仅提供了signal形式的api。 因为我们可以非常低成本的接入,且支持和native api混用。

import { useState, useEffect } from 'react';
import { useSignal, useEffect as useEffect2 } from '@ivliu/react-signal';

function App(props: { count3: number }) {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useSignal(0);

  useEffect(() => {
    console.log(count1, count2(), props.count3);
  }, [count1, count2, props.count3]);

  useEffect2(() => {
    console.log(count1, count2(), props.count3);
    // state和props值无法自动追踪,需要显式声明依赖
  }, [count1, props.count3]);

  return <div onClick={() => {
    setCount1(count1 + 1);
    setCount2(count2() + 1);
  }}>{count1 + count2() + props.count3}</div>
}

与useState的不同

在使用useSignal的时候需要注意和useState的不同

function App1() {
  const [count, setCount] = useState(0);

  return (
    <p onClick={() => {
      // 点击一次,count值加1
      setCount(count + 1);
      setCount(count + 1);
      setCount(count + 1);
    }}>{count}</p>
  )
}

function App2() {
  const [count, setCount] = useSignal(0);

  return (
    <p onClick={() => {
      // 点击一次,count值加3,因为signal是稳定且可变的
      setCount(count() + 1);
      setCount(count() + 1);
      setCount(count() + 1);
      // 如果你想保持行为一致,你需要
      // const current = count();
      // setCount(current + 1);
      // setCount(current + 1);
      // setCount(current + 1);
    }}>{count}</p>
  )
}

todo

在native effect中我们可以自由控制监听的粒度,比如

// native effect
useEffect(() => { console.log(person) }, [person.name]);

但目前react-signal只能做到signal粒度的自动追踪,我们正在努力实现该feature。 如果你想实现类似效果,你可以暂时这样做。

useEffect(() => { console.log(untrack(() => person())) }, [person().name]);

贡献

请随时提交任何问题或请求请求。我将在最快的时间回复你。

License

MIT

Keywords

javascript

FAQs

Package last updated on 17 Mar 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts