Git Product home page Git Product logo

signals's Introduction

Signals

Signals is a performant state management library with two primary goals:

  1. Make it as easy as possible to write business logic for small up to complex apps. No matter how complex your logic is, your app updates should stay fast without you needing to think about it. Signals automatically optimize state updates behind the scenes to trigger the fewest updates necessary. They are lazy by default and automatically skip signals that no one listens to.
  2. Integrate into frameworks as if they were native built-in primitives. You don't need any selectors, wrapper functions, or anything else. Signals can be accessed directly and your component will automatically re-render when the signal's value changes.

Read the announcement post to learn more about which problems signals solves and how it came to be.

Installation:

# Just the core library
npm install @preact/signals-core
# If you're using Preact
npm install @preact/signals
# If you're using React
npm install @preact/signals-react
# If you're using Svelte
npm install @preact/signals-core

Guide / API

The signals library exposes four functions which are the building blocks to model any business logic you can think of.

signal(initialValue)

The signal function creates a new signal. A signal is a container for a value that can change over time. You can read a signal's value or subscribe to value updates by accessing its .value property.

import { signal } from "@preact/signals-core";

const counter = signal(0);

// Read value from signal, logs: 0
console.log(counter.value);

// Write to a signal
counter.value = 1;

Writing to a signal is done by setting its .value property. Changing a signal's value synchronously updates every computed and effect that depends on that signal, ensuring your app state is always consistent.

signal.peek()

In the rare instance that you have an effect that should write to another signal based on the previous value, but you don't want the effect to be subscribed to that signal, you can read a signals's previous value via signal.peek().

const counter = signal(0);
const effectCount = signal(0);

effect(() => {
	console.log(counter.value);

	// Whenever this effect is triggered, increase `effectCount`.
	// But we don't want this signal to react to `effectCount`
	effectCount.value = effectCount.peek() + 1;
});

Note that you should only use signal.peek() if you really need it. Reading a signal's value via signal.value is the preferred way in most scenarios.

computed(fn)

Data is often derived from other pieces of existing data. The computed function lets you combine the values of multiple signals into a new signal that can be reacted to, or even used by additional computeds. When the signals accessed from within a computed callback change, the computed callback is re-executed and its new return value becomes the computed signal's value.

import { signal, computed } from "@preact/signals-core";

const name = signal("Jane");
const surname = signal("Doe");

const fullName = computed(() => name.value + " " + surname.value);

// Logs: "Jane Doe"
console.log(fullName.value);

// Updates flow through computed, but only if someone
// subscribes to it. More on that later.
name.value = "John";
// Logs: "John Doe"
console.log(fullName.value);

Any signal that is accessed inside the computed's callback function will be automatically subscribed to and tracked as a dependency of the computed signal.

effect(fn)

The effect function is the last piece that makes everything reactive. When you access a signal inside its callback function, that signal and every dependency of said signal will be activated and subscribed to. In that regard it is very similar to computed(fn). By default all updates are lazy, so nothing will update until you access a signal inside effect.

import { signal, computed, effect } from "@preact/signals-core";

const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);

// Logs: "Jane Doe"
effect(() => console.log(fullName.value));

// Updating one of its dependencies will automatically trigger
// the effect above, and will print "John Doe" to the console.
name.value = "John";

You can destroy an effect and unsubscribe from all signals it was subscribed to, by calling the returned function.

import { signal, computed, effect } from "@preact/signals-core";

const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);

// Logs: "Jane Doe"
const dispose = effect(() => console.log(fullName.value));

// Destroy effect and subscriptions
dispose();

// Update does nothing, because no one is subscribed anymore.
// Even the computed `fullName` signal won't change, because it knows
// that no one listens to it.
surname.value = "Doe 2";

The effect callback may return a cleanup function. The cleanup function gets run once, either when the effect callback is next called or when the effect gets disposed, whichever happens first.

import { signal, effect } from "@preact/signals-core";

const count = signal(0);

const dispose = effect(() => {
	const c = count.value;
	return () => console.log(`cleanup ${c}`);
});

// Logs: cleanup 0
count.value = 1;

// Logs: cleanup 1
dispose();

batch(fn)

The batch function allows you to combine multiple signal writes into one single update that is triggered at the end when the callback completes.

import { signal, computed, effect, batch } from "@preact/signals-core";

const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);

// Logs: "Jane Doe"
effect(() => console.log(fullName.value));

// Combines both signal writes into one update. Once the callback
// returns the `effect` will trigger and we'll log "Foo Bar"
batch(() => {
	name.value = "Foo";
	surname.value = "Bar";
});

When you access a signal that you wrote to earlier inside the callback, or access a computed signal that was invalidated by another signal, we'll only update the necessary dependencies to get the current value for the signal you read from. All other invalidated signals will update at the end of the callback function.

import { signal, computed, effect, batch } from "@preact/signals-core";

const counter = signal(0);
const double = computed(() => counter.value * 2);
const triple = computed(() => counter.value * 3);

effect(() => console.log(double.value, triple.value));

batch(() => {
	counter.value = 1;
	// Logs: 2, despite being inside batch, but `triple`
	// will only update once the callback is complete
	console.log(double.value);
});
// Now we reached the end of the batch and call the effect

Batches can be nested and updates will be flushed when the outermost batch call completes.

import { signal, computed, effect, batch } from "@preact/signals-core";

const counter = signal(0);
effect(() => console.log(counter.value));

batch(() => {
	batch(() => {
		// Signal is invalidated, but update is not flushed because
		// we're still inside another batch
		counter.value = 1;
	});

	// Still not updated...
});
// Now the callback completed and we'll trigger the effect.

untracked(fn)

In case when you're receiving a callback that can read some signals, but you don't want to subscribe to them, you can use untracked to prevent any subscriptions from happening.

const counter = signal(0);
const effectCount = signal(0);
const fn = () => effectCount.value + 1;

effect(() => {
	console.log(counter.value);

	// Whenever this effect is triggered, run `fn` that gives new value
	effectCount.value = untracked(fn);
});

License

MIT, see the LICENSE file.

signals's People

Contributors

andrewiggins avatar billybimbob avatar codyjasonbennett avatar dcporter avatar developit avatar eddyw avatar elliotwaite avatar github-actions[bot] avatar iahu avatar jaredcwhite avatar jhuleatt avatar jmeistrich avatar jongwooo avatar jossmac avatar jovidecroock avatar jviide avatar keichinger avatar kim3er avatar marvinhagemeister avatar melnikov-s avatar nazimamin avatar nichoth avatar obahareth avatar pauan avatar prinsss avatar rschristian avatar skvale avatar sun0day avatar tjenkinson avatar xantredev 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

signals's Issues

Infinite loop with `+=`

Not sure what to do with this:

const a = signal("a");
const b = signal("b");

effect(() => {
  // Causes infinite loop because "b" is read inside effect
  b.value += a.value;
});

Add batching/transaction

With synchronous execution like we have, it might be desirable to apply multiple updates together instead of individually. There is often no need to re-evaluate all computeds before all mutations have taken place.

batch(() => {
  signalA.value = "A";
  signalB.value = "B";
});

Expected behaviors:

  • transitions can be nested, only resolve once the outermost completes.
  • refresh only the signals inside a transaction when they're read from immediately after.
batch(() => {
  signal.value = 1;
  consoe.log(signal.value); // Should log: 1
});

Recommend this post by the author of MobX about its inner workings: https://medium.com/hackernoon/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254

A computed value is updated even when all of its dependencies haven't changed.

From reading over the logic in core, I think I found something that could go wrong. I created this test, which currently isn't passing, to illustrate the issue:

it("should not update a sub if all deps unmark it", () => {
    // In this scenario "B" and "C" always return the same value. When "A"
    // changes, "D" should not update.
    //     A
    //   /   \
    // *B     *C
    //   \   /
    //     D
    const a = signal("a");
    const b = computed(() => {
        a.value;
        return "b";
    });
    const c = computed(() => {
        a.value;
        return "c";
    });
    const spy = sinon.spy(() => b.value + " " + c.value);
    const d = computed(spy);
    expect(d.value).to.equal("b c");
    spy.resetHistory();

    a.value = "aa";
    expect(spy).not.to.be.called; // <- this assertion currently does not pass.
});

What I think happens is that when a is updated, it calls mark recursively down the tree starting at a resulting in:

a._pending = 1
b._pending = 1
c._pending = 1
d._pending = 2

Then, the sweep starts at a, then goes to b where it calls b's _updater which recomputes b and since its value is the same, it calls unmark on b's subs which are d, and in that unmark for d it decrements d._pending bringing it to 1. Then were back in the sweep call for b and we call sweep on all of b's subs, which is d. So then in the sweep call for d the if (--signal._pending === 0) { condition decrements d._pending to 0 and evaluates to true, which causes d to recompute unnecessarily.

At least that's what I think is happening from reading the code. I haven't checked it with a debugger.

The issue seems to be related to the fact that both unmark and sweep can decrement the _pending value, and a node that returns the same value can both unmark its subs and then sweep its subs, which can double decrement their _pending value, which I would think is unintended, but I'm not quite sure, I'm still trying to wrap my head around this code.

Is the intended mark and sweep algorithm a standard one that I could read more about somewhere, or is the specific algorithm that is used somewhat unique?

I haven't thought too much about a solution to this issue yet, since I figured those more familiar with the code and the intended algorithm would have a better idea of what to change, but I'd be open to working on a solution if help in that area is wanted.

Regression: unexpected non-computed batch behavior

I have been testing preact/signals-core together with usignal and solid-js but after installing latest my tests, which run just fine with latter two libraries and used to run just fine before latest release, now fail with preact.

The test in case contains this code:

const {assert} = console;

(function testBatchMore() {
  const invokes = [];
  const counter = signal(0);
  const double = computed(() => counter.value * 2);
  const tripple = computed(() => counter.value * 3);

  effect(() => {
    invokes.push([double.value, tripple.value]);
  });

  assert(invokes.length === 1, 'effect not working in bached more');

  batch(() => {
    counter.value = 1;
    // here double.value is actually 0 instead!
    assert(double.value === 2, 'computed side-effecting within batch');
  });

  assert(invokes.length === 2, 'unexpected more batched result');
  assert(invokes[0].join(',') === '0,0', 'unexpected batched more values');
  assert(invokes[1].join(',') === '2,3');
})();

If you'd like to test yourself:

# git clone ... usignal
cd usignal
npm i
cd test
npm i
cd ..
npm test preact
# see difference with
# npm test solid
# npm test usignal

If this regression was actually an intentional change, it would be great to understand how come preact signals decided to behave differently from solid-js there (I am just the last one playing around with signals here, I don't have strong opinion about anything 😊)

Thanks.

Nested effects.

I have an issue if I nest effects like this:

const counter = signal(1);
const double = computed(() => counter.value * 2);
const tripple = computed(() => counter.value * 3);

effect(() => {
  console.log(double.value);
  effect(() => {
    console.log(tripple.value);
  });
});
counter.value = 20;

The triple value is consoled 3 times.

Define signals() shape

1) Getter

const name = signal("Jane");
const surname = signal("Doe");

const full = computed(() => name.value + " " + surname.value);
console.log(full.value);

// write
name.value = "John";

Pros

  • Avoids invoking iterator protocol
  • Easy to integrate with Preact v11
  • Similar to Vue
  • TS can infer control flow out of the box
    const foo = signal<string | null>("Jane");
    
    if (typeof foo.value === "string") {
      // works, detected as string
      const upper = foo.value.toUpperCase();
    }

Cons

  • How do we force readonly signals as a return value for computed? We could leverage some TS vodoo, but that won't help non-TS users
    const name = signal("Jane");
    const surname = signal("Doe");
    
    const full = computed(() => name.value + " " + surname.value);
    
    // How do we avoid this?
    full.value = "nope";
  • Requires eager update in batch()
    const name = signal("Jane");
    const surname = signal("Doe");
    
    // Will flush updates after callback completes
    batch(() => {
      name.value = "Foo";
    
      // Force read, expected to be "Foo"
      console.log(name.value);
    });
  • value.value might be common for input fields

Open questions

  • Should we use .$ instead of .value?
    • e.g. value.value vs value.$

2) Solid-style (hooks-like)

const [name, setName] = signal("Jane");
const [surname] = signal("Doe");

const full = computed(() => name() + " " + surname());
console.log(full());

// write
setName("John");

Pros

  • Clear distinction between reads and writes
  • Writable signals return[value(), setValue] vs readable return only value()
  • Makes it impossible to write to a readonly signal
  • Might need an additional wrapper for Preact v11. With getters we can leverage .data getter for Text nodes
  • Familiar to hooks, only difference: value is a function
  • Familiar to SolidJS users
  • Avoids need for intermediate updates in batch()
    const [name, setName] = signal("Jane");
    const [surname] = signal("Doe");
    
    // Will flush updates after callback completes
    batch(() => {
      setName("Foo");
    
      // Force read, we can still return "Jane"
      console.log(name());
    });

Cons

  • TS cannot detect control flow, value needs to be unwrapped first
    const [name] = signal<string | null>("Jane");
    
    if (typeof name() === "string") {
      // TS throws error that `name()` can be null
      const upper = name().toUpperCase();
    }
    
    // solution
    const foo = name();
    if (typeof foo === "string") {
      const upper = foo.toUpperCase();
    }

3) Getter hooks like

const [name, setName] = signal("Jane");
const [surname] = signal("Doe");

const full = computed(() => name.value + " " + surname.value);
console.log(full.value);

// write
setName("John");

Pros

  • Similar to hooks
  • separates reads + writes

Cons

  • tbd

Preact's TS definitions don't allow signals to be used in the "value" prop of an <input />

Current type definitions for Preact don't allow the use of a signal in the value prop of an <input /> element, like for example when trying to create a typical two-way data binding. Attempting to do something like:

<input type="text" value={input} onInput={e => input.value = e.currentTarget.value} />

gives you the TS error:

Type 'Signal<string>' is not assignable to type 'string | number | string[] | undefined'.

At this moment, you either have to manually recast it (e.g. input as any) or use the .value property of the signal which causes unnecessary re-renders of the component.

Looped effects don't cleanup after each-other

I am not sure if this is related to other bugs but I'll leave here another test I've added to usignal to code cover that case too:

  const {assert} = console;
  (function loopedEffects() {
    const invokes = [];
    const num = signal(0);
    let loop = 2;

    effect(() => {
      invokes.push(num.value);
      for (let i = 0; i < loop; i++)
        effect(() => {
          invokes.push(num.value + i);
        });
    });

    assert(invokes.length === 3, 'looped effects not working');
    assert(invokes.join(',') === '0,0,1', 'looped values not matching');
  
    invokes.splice(0);
    loop = 1;
    num.value = 1;

    // this is actually 4 in preact, usignal and solid-js work just fine here
    assert(invokes.length === 2, 'looped effects not working after changes');
    assert(invokes.join(',') === '1,1', 'looped values not matching after changes');
  })();

The logic of the test is that shrinking loops should not side-effect previous loops not part of the outer effect anymore.

Feel free to ask more, if needed 👋

Why am I getting a "Cycle detected" error?

Hey, I am new to signals. This looks like an amazing tool I am planning to use in my future developments. I just try @preat/signals using preact.js but have the following error every time I try to update the value of the signal I have created.

Uncaught Error: Cycle detected
    at index.ts:168:17
    at Set.forEach (<anonymous>)
    at h2 (index.ts:160:7)
    at index.ts:175:5
    at Set.forEach (<anonymous>)
    at h2 (index.ts:160:7)
    at e3.set (index.ts:84:5)
    at Object.onChange [as changefalse] (Test.jsx:19:29)
    at HTMLInputElement.I (props.js:153:20)
(anonymous) @ index.ts:168
h2 @ index.ts:160
(anonymous) @ index.ts:175
h2 @ index.ts:160
set @ index.ts:84
onChange @ Test.jsx:19
I @ props.js:153

Any idea?

Thanks a lot.

Only updated signals when they're subscribed to

Currently we're eagerly updating all downstream computeds that ultimately depend on the signal we've written to. But some of them might not be subscribed to by anyone.

const count = signal(0);
// Will be updated eagerly, despite nobody subscribing to it
computed(() => count.value);

This can happen when the user has a global store which holds computeds that only certain elements in the ui subscribe to.

Support for writable signals with `computed()`

Is it possible to support writes for signals returned via the computed function?
My intuition is that the computed function could simply take a second argument for the 'set' operation.

const user = new signal({ id: 1, name: 'foo' });
const name = computed(
  () => user.name,
  val => user.value = ({ ...user, name: value })
);

This has probably been considered already, but I'd be curious to hear if it's feasible or what the challenges would be.

effect is run during SSR (React version)

Having effect(() => console.log('hello')) in a component prints in the console of a Next.js project, so it's being run on the server.

Is it a bug? If not, how to use effect on the client-only? (which is what most people would want I think).

Signals don't work after HMR

Vite preact-ts starter with React compat.

import { signal } from "@preact/signals";
import { Button, css, TextField } from "@mui/material";
import { useState } from "preact/hooks";

const count = signal(0);

export function App() {
  return (
    <div className="flex">
      <Button variant="contained" onClick={() => count.value++}>
        Count {count}
      </Button>
      <TextField></TextField>
    </div>
  );
}
pnpm dev

Save the page, signals stop working (can't increase count).

Launch Checklist

  • Decide on a name
  • Do a test run with porting devtools' state management over
  • Add Preact bindings
  • Port devtools to Preact bindings
  • Announcement post
  • docs

pnpm unmet peer react@"17.x | 18.x": found 17.0.2

i tryed to install "@preact/signals-react": "^1.0.0", using pnpm 7.11.0 and got the following error:

.
└─┬ @preact/signals-react 1.0.0
  └── ✕ unmet peer react@"17.x | 18.x": found 17.0.2

so 17.0.2 should work fine?
i am not sure if its a pnpm bug but may you could change the peerDependencies in a way, that pnpm understands it.

Extend a signal API to match the Svelte store contract

First of all, thank you for the library!

I'd like to suggest a proposal to extend the usage of the signals-core library to the Svelte framework.
I mean an implementation of a Svelte's store contract, which is about a subscribe method: 

type Subscriber<T> = (value: T) => void;
type Unsubscriber = () => void;
subscribe(run: Subscriber): Unsubscriber;

So, the signals would need to have an additional subscribe method that returns a function that lets you unsubscribe.
The Nanostore library does exactly that: https://github.com/nanostores/nanostores#svelte, so developers can use atoms without any dedicated adapter.
I'm sure the advantages of the ability to subscribe to a certain signal would exceed the Svelte support.

More info about Svelte's stores can be found here:
https://svelte.dev/docs#run-time-svelte-store
https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts

Thanks

@preact/signals-react crashes on production build, at least using `react-scripts build`

cf: #86

The error here is: TypeError: Invalid value used as weak map key

image

What appears to be the case is that internals.ReactCurrentOwner.current is not set.

Here are the versions I have installed:

    "@preact/signals-react": "^1.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.4.2",

and the browser configuration:

  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }

How to reproduce:

  1. using create-react-app (typescript template), create a react app
  2. use @preact/signals-react
  3. yarn build (creates a production build)
  4. open your built browser-app in the browser
  5. behold this error

React skips the first rerender in StrictMode

react: 18.2.0
@preact/signals-react: 1.0.0

When <StrictMode /> is wrapping <App />, the first rerender of every signal-attached component is ignored.

To reproduce:

  1. Enter https://codesandbox.io/s/amazing-glitter-6eyndo?file=/src/index.js
  2. Click on one of the buttons.
  3. You can see that the new value logs correctly, but React simply won't repaint the screen.
  4. Click on the same button again and react will "skip" and render the new value (the previous one was ignored).
  5. Now remove StrictMode wrapping from index.js and repeat the previous steps, everything should work properly.

I personally don't use StrictMode, I just wanted to let you know that this problem exists.

Thanks for this amazing project, it really is revolutionary!

Typo in invalidated batch example in README

In the following example:

import { signal, computed, effect, batch } from "@preact/signals-core";

const counter = signal(0);
const double = computed(() => counter.value * 2);
const tripple = computed(() => counter.value * 3);

effect(() => console.log(double.value, tripple.value));

batch(() => {
	counter.value = 1;
	// Logs: 2, despite being inside batch, but `tripple`
	// will only update once the callback is complete
	console.log(counter.double);
});
// Now we reached the end of the batch and call the effect

I think counter.double should read double.value.

Trying to understand preact/signals’ private options hooks usage

Basically, I’m trying to understand how or where the hook options.__b gets called. Doing a stack trace does show that it’s called when options._diff gets called. But I fail to see the code responsible for setting that up.

I’m doing some tooling involving preact/signals, so I’m trying to understand some of the internals. Hope someone can shed some light for me. Thanks!

Add a way to detect if a signal is writable

Came across this use case in an application where a model has a reset() method that clears all values. To do that they just iterate over the model's members and check if each field is writable or not.

We currently don't expose a way to detect if the signal is writable or not.

Using signals does not trigger re-render after updating the signal

Description

I'm trying to experiment with signals in Preact and found out that sometimes updating the value does not always trigger re-render when it should in a component. I would like to know if I'm using signals the correct way.

This is a minimal reduced test case to demonstrate the problem. The basic behavior is as follows:

  1. When there is no otpCode, hitting Enter in the Code input will trigger a countdown timer and you should see Resend in 00:10
  2. When there is otpCode, hitting Enter will evaluate fakeValid. When fakeValid is true, there will be no Resend in 00:10 and Send code button, and Code input will be in read-only mode.

Expected behavior

Re-render should happen after updating signal value, e.g. updating otpCode and otpStatus does not re-render.

Actual behavior

Re-render does not always happen after updating signal value, e.g. updating otpCode and otpStatus should re-render accordingly.

React.StrictMode misses state update (using with ViteJS)

I know this is super early days and this looks really exciting!

I tried setting up a new TypeScript + React project from scratch with ViteJS and using your counter example the first count+ click doesn't update the UI. However the second does so counter goes from 0->2->3 etc. When disabling React.StrictMode it works however. Just a heads up. Thank you for this project!

FYI: Uses React 18.2.

`preact-cli` isn't able to SSR if the app uses `@preact/signals`

Just started to use signals in an app I created using preact-cli, but now the SSR breaks with the following error:

❯ npm run build

> [email protected] build
> preact build

 Build  [==================  ] 92% (4.8s) chunk asset optimizationUnable to read file: /Users/rakesh.pai/code/my-project/src/src/index.ts

TypeError: Cannot read properties of null (reading '_u')
Cannot determine error position. This most likely means it originated in node_modules.

This could be caused by using DOM or Web APIs.
Pre-render runs in node and has no access to globals available in browsers.

Consider wrapping code producing error in: "if (typeof window !== "undefined") { ... }"

Alternatively use "preact build --no-prerender" to disable prerendering.

See https://github.com/preactjs/preact-cli#pre-rendering for further information.

This error message itself isn't very useful (and I don't know why there's an additional /src in the file path), but this error doesn't occur and the build runs successfully if I remove signals.

The usage of signals is very minimal:

import { computed, signal } from '@preact/signals';

const currentState = signal('idle');

const isFormEnabled = computed(() => currentState.value !== 'loading');

const SomeComponent = () => (
  <form>
    <input type="text" disabled={!isFormEnabled.value} />
  </form>
)

Am I doing something wrong?

Reactive/Store-like primitive

// Turns properties into signals
const r = reactive({ foo: 123, bar: "hehe" });

console.log(r.foo.value);

Open question: Should conversion be deep or shallow?

Pros

  • Easy to make an existing object reactive

Cons

  • tbd

Update with assign()-syntax

// Update a Reactive with assign()-like syntax:
const r = reactive({ name: "Alice" });
update(r, { name: "Bob" });

// property 'age' does not exist in type '{ name?: string }'
update(r, { age: 42 });

// '2' has no properties in common with '{ name?: string }'
update(r, 2);

console.log(r.name.value); // "Bob"

Pros

  • Good for Redux users?

Cons

  • Ambiguity for collections/arrays: Should we update or concat?
  • Is this needed for an MVP or could this be added in users themselves? What's the use case?
  • Risks forking update logic

bug: TypeScript 4.8 unable to discover @preact/signals for auto import

Description

Not sure if this is the right place to post this TypeScript specific issue but I found out that auto importing @preact/signals fails in VSCode when using latest TypeScript 4.8. Auto importing works just fine when downgrading to TypeScript 4.7.

Affected TypeScript version: 4.8.3
Affected @preact/signals version: 1.0.3

Steps to reproduce

  1. Create a .ts or .tsx file with simple TSConfig file.
  2. Write signals to trigger VSCode's autocompletion to auto import signals from @preact/signals

Expected behavior

Auto imports should work when typing signals

Actual behavior

Auto imports fails to auto import anything from @preact/signals when using TypeScript 4.8.

Signals do not work when used as attribute values on SVG elements

Tried something like this:

import type { ReadonlySignal } from '@preact/signals';
import type { JSX } from 'preact';

type HandProps = {
  length: number;
  limit?: number;
  stationary?: boolean;
  transform: ReadonlySignal<string>,;
} & Omit<JSX.SVGAttributes<SVGLineElement>, 'transform'>;

export const Hand = ({
  className = '',
  length = 0,
  limit = 90,
  stationary,
  transform,
  ...rest
}: HandProps) => (
  <line
    className={`stroke-cap-round ${className}`}
    transform={transform}
    y1={stationary ? length - limit : undefined}
    y2={-(stationary ? limit : length)}
    {...rest}
  />
);

but it doesn't work. I know the support is still experimental, just wanted to report this, as this works for other elements, I'm seeing this on SVG elements. TypeScript will show an error, but I think there's an issue regarding that.

signals-react doesn't work in react production build

The basic example is not working on production build in react:

const count = useSignal(1);
return (
<div>
   {count}
   <button onClick={() => count.value++}>INC</button>
</div>
);

Versions:

    "@preact/signals-react": "^1.0.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",

Building with vite 3.1.0

signals in react (three-fiber) doesn't work transiently

codesandbox: https://codesandbox.io/s/interesting-panna-urdtu6?file=/src/App.js

import { signal } from '@preact/signals-react'

const opacity = signal(1)

setTimeout(() => {
  // Should be transparent in 3 seconds ...
  // Works with opacity={opacity.value} but then it re-renders
  // Doesn't work as opacity={opacity}
  opacity.value = 0.5
}, 3000)

function Box() {
  return (
    <mesh>
      <boxGeometry />
      <meshBasicMaterial transparent color="orange" opacity={opacity} />
    </mesh>
  )
}

it's not a div, but it would be fantastic if it could work.

How to use with `unpkg`

Hello there,

first of all: This is a great extension to Preact and just what has been missing to me to start using Preact in the first place, as the whole Context concept to achieve similar things seems unnecessarily complicated.

As I like to avoid using any build tools at all and since it is possible to use Preact itself just by using import { h, Component, render } from 'https://unpkg.com/preact?module'; I am wondering how to do the same with signals?

The obvious solution to be would be to import it like import { signal } from 'https://unpkg.com/@preact/[email protected]/dist/signals.module.js?module';, but this will fail, as the resource https://unpkg.com/[email protected]/hooks?module is not available.

So is there a single, or multiple, import statement(s) to completely load all the required Preact tools, including h, render, Component as well as the new functions signal, computed etc.?

Thank you very much for your support!

Sincerly,
Happy Striker

Consider implementing `untrack` or allow value updates without subscribing

Since accessing .value of a signal subscribes to any future updates, this can cause some unwanted rerunning of effects/infinite render loops.

I think it would be a good idea to implement a way to read and update the value without triggering a subscription. One way to solve this would be with something like untrack. This would be a function similar to effect:

effect(() => {
  const value = signal1.value                      // Triggers reactivity when signal1 changes
  untrack(() => signal2.value += value)     // Updates signal2, but does not trigger when signal2 changes
})

The other way I can think of is by adding a update function to a signal:

effect(() => {
  signal2.update((s2) => s2 + signal1.value) // signal1 triggers effect, but signal2 doesn't
})

Errors when Using Signals in React

hey , i am new to signals . today I just tried @preact/signals-react . in UI it is working fine in updating values but i am getting an error in the console when importing import { signal } from "@preact/signals-react";

you can check the error in the image below

Screenshot (4322)

Data validation

Hello,

thanks for your work, signals looks amazing! However, I got 1 question. While using Context API for store, user have option to validate data payload based on different actions and then mutate store or throw / log errors. How could this be achieved with signals?

E. g.: Mutate signal only if data payload is ISO 8601 date representation (YYYY-MM-DD), where date is greater than now. Should we write wrappers for validation and initialize them in each component with signal retrieved via Context API?

Thanks.

React has detected a change in the order of Hooks called by Root


Screenshot 2022-09-19 at 2 31 17 PM

I'm using Next.js, when I navigated to another page, this error occurred

this is the target page code

const count = signal(0);

function Counter() {
  return <button onClick={() => count.value++}>Value: {count.value}</button>;
}
export default Counter;
  • next: 12.2.5
  • react: 18.2.0
  • signals-react: 1.0.2

Error handling issue

From @developit on slack:

when part of the graph bails out due to an error in a computed, re-assigning the value of one of its dependencies doesn't re-run the errored computed - technically correct, since reassignment to the same value shouldn't re-run the graph
I don't think its a bug, but its something we might want to have a doc on.

Rollback commits on error?

Found by @developit :

When a computed errors me might want to roll back the whole commit instead of just the subtree. Doing the latter is fine for now, but ultimately ends up with an inconsistent graph.

Test case: https://gist.github.com/developit/e9d25d6bf38dfbf1ddec13f92b4b3738

An earlier discussion:

when part of the graph bails out due to an error in a computed, re-assigning the value of one of its dependencies doesn't re-run the errored computed - technically correct, since reassignment to the same value shouldn't re-run the graph
I don't think its a bug, but its something we might want to have a doc on.

Any recommendations on how to structure complex data?

I'm trying to wrap my head around using signals for our app. Should I use nested signals for complex data?

For example, the todos example. What if I don't want to update all todos when a single todo changes? Should I do the following?

const todos = signal([
  signal({ text, done }),
  signal({ text, done })
])

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.