why doesn't signal re-render when add a new item? Preact signals

460 Views Asked by At

I'm learning signals features and met some issues:

  1. if set value={newTodo.value} then I cant type in input field, there would be only one character be typed in.
  2. if set value={newTodo}, I can type in input field, but my todo list todos never update/re-render in UI when I click the button to add a new todo item even tho console.log shows the list is updated already.

But everything would works fine if i change back to react hooks. Can anyone help? Thanks!

I also created a sandbox for this issue.

import { signal, effect } from "@preact/signals-react"
import { useState } from "react"

export default function SignalsTodolist() {
  const todos = signal(["eat", "sleep", "play video game"])
  const newTodo = signal("")

  const addNewTodo = (e) => {
    e.preventDefault()
    todos.value = [...todos.value, newTodo.value]
    console.log(todos.value)
    newTodo.value = "" // Use setNewTodo to update the state
  }

  // const resetToto = () => {
  //   newTodos(["eat", "sleep", "play video game"])
  // }

  return (
    <div>
      Create a todo list with signals。
      <br />
      <br />
      <input
        type="text"
        value={newTodo}.   // highlight
        onChange={(e) => (newTodo.value = e.target.value)}
        placeholder="Type to add..."
      />
      <button type="submit" onClick={addNewTodo}>
        add new todo
      </button>
      <br />
      {newTodo}, {todos} {/* To display the array */}
      <ul>
        {todos.value.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  )
}

enter image description here

3

There are 3 best solutions below

1
rschristian On BEST ANSWER

You're running into two usage issues at once:

  1. You cannot use signal or computed in components. Use useSignal or useComputed.

When creating signals within a component, use the hook variant: useSignal(initialValue).

https://preactjs.com/guide/v10/signals/#signalinitialvalue

  1. You've skipped over the React integration instructions. Usage changed in v2, you need to pick between using the Babel plugin or useSignals().
1
Oktay Yuzcan On

signal(...) creates new instance on each render. When you update it, rerender happens and then signal gives you the initial instance. You should not use it inside component. Move it outside or use useSignal

https://preactjs.com/guide/v10/signals/#local-state-with-signals

6
Kyle Xyian Dilbeck On

After some research....maybe attempt to use batch to make sure items are updated/rerendered.

preact is very fragile in the sense that the smallest thing can mess it up - ive been messing with a few different sandboxes and this seems to be the best working example but has a lot of overhead for a simple todo list https://codesandbox.io/p/sandbox/preact-signals-react-5qcp8r

TodoList.js

import { signal, computed, batch } from "@preact/signals-react";
import { useAppState } from "./AppStateContext";

const newItem = signal("");

export default function TodoList() {
  const { todos, addTodo, removeTodo } = useAppState();
  const completedCount = computed(() => {
    return todos.value.filter((todo) => todo.completed).length;
  });

  const onInput = (event) => (newItem.value = event.target.value);

  const onAddClick = () => {
    batch(() => {
      addTodo(newItem);
      newItem.value = "";
    });
  };

  return (
    <>
      <input type="text" value={newItem.value} onInput={onInput} />
      <button style={{ marginLeft: "10px" }} onClick={onAddClick}>
        Add
      </button>
      <ul style={{ textAlign: "left" }}>
        {todos.value.map((todo, index) => {
          return (
            <li>
              <input
                type="checkbox"
                checked={todo.completed}
                onInput={() => {
                  todo.completed = !todo.completed;
                  todos.value = [...todos.value];
                }}
              />
              {todo.completed ? <s>{todo.text}</s> : todo.text}{" "}
              <button
                style={{ marginLeft: "10px", color: "red" }}
                onClick={() => removeTodo(index)}
              >
                x
              </button>
            </li>
          );
        })}
      </ul>
      <p>Completed count: {completedCount.value}</p>
    </>
  );
}

AppContext.js

import React, { useContext } from "react";
import { signal } from "@preact/signals-react";
const AppState = React.createContext();

function AppStateProvider(props) {
  const todos = signal([
    { text: "eat", completed: true },
    { text: "sleep", completed: false },
    { text: "rave", completed: false },
    { text: "repeat", completed: false }

  ]);
  function addTodo(newItem) {
    todos.value = [...todos.value, { text: newItem.value, completed: false }];
  }

  function removeTodo(index) {
    todos.value.splice(index, 1);
    todos.value = [...todos.value];
  }

  return (
    <AppState.Provider value={{ todos, addTodo, removeTodo }}>
      {props.children}
    </AppState.Provider>
  );
}

function useAppState() {
  const appState = useContext(AppState);
  if (!appState) {
    throw new Error("useAppState must be used within a AppStateProvider");
  }
  return appState;
}

export { AppStateProvider, useAppState };

from what ive heard from coworkers that have been at companies which use preact, unless you use it perfectly everytime, signals etc can become a nightmare. Also your code issue could be because of how preact is interacting with your framework because as the preact docs state, when you update value it should cause a re-render but it obviously is not

even in the example given, the state is not working fully as expected...try deleting unchecked items then checking a new item or add item after trying to delete unchecked..youll see some obvious render issues..