npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ivliu/react-signal

v1.0.8

Published

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

Downloads

84

Readme

@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