diff --git a/content/en/guide/v10/components.md b/content/en/guide/v10/components.md index 5a5a18452..d835f36bb 100755 --- a/content/en/guide/v10/components.md +++ b/content/en/guide/v10/components.md @@ -35,6 +35,8 @@ const App = ; render(App, document.body); ``` +Functional components often use Hooks to manage state, but Preact also provides [Signals](/guide/v10/signals). Signals are a reactive state primitive that can be used **inside or outside** components, offering fine-grained updates and a reactivity model many users find to be simpler. They're a great option for managing state in modern Preact apps. + > Note in earlier versions they were known as `"Stateless Components"`. This doesn't hold true anymore with the [hooks-addon](/guide/v10/hooks). ## Class Components diff --git a/content/en/guide/v10/forms.md b/content/en/guide/v10/forms.md index ff0410aaa..002cb722e 100755 --- a/content/en/guide/v10/forms.md +++ b/content/en/guide/v10/forms.md @@ -21,7 +21,7 @@ Often you'll want to collect user input in your application, and this is where ` To get started, we'll create a simple text input field that will update a state value as the user types. We'll use the `onInput` event to listen for changes to the input field's value and update the state per-keystroke. This state value is then rendered in a `

` element, so we can see the results. - + ```jsx // --repl @@ -68,11 +68,32 @@ function BasicInput() { render(, document.getElementById('app')); ``` +```jsx +// --repl +import { render } from 'preact'; +import { useSignal } from '@preact/signals'; +// --repl-before +function BasicInput() { + const name = useSignal(''); + + return ( +

+ +

Hello {name}

+
+ ); +} +// --repl-after +render(, document.getElementById('app')); +``` + ### Input (checkbox & radio) - + ```jsx // --repl @@ -194,11 +215,80 @@ function BasicRadioButton() { render(, document.getElementById('app')); ``` +```jsx +// --repl +import { render } from 'preact'; +import { useSignal, useComputed } from '@preact/signals'; +import { Show } from '@preact/signals/utils'; +// --repl-before +function BasicRadioButton() { + const allowContact = useSignal(false); + const contactMethod = useSignal(''); + + const setRadioValue = e => (contactMethod.value = e.currentTarget.value); + const isDisabled = useComputed(() => !allowContact.value); + const contactStatus = useComputed( + () => `have allowed via ${contactMethod.value}` + ); + + return ( +
+ + + + +

+ You{' '} + + {() => contactStatus} + +

+
+ ); +} +// --repl-after +render(, document.getElementById('app')); +``` +
### Select - + ```jsx // --repl @@ -244,7 +334,30 @@ function MySelect() {

You selected: {value}

- + + ); +} +// --repl-after +render(, document.getElementById('app')); +``` + +```jsx +// --repl +import { render } from 'preact'; +import { useSignal } from '@preact/signals'; +// --repl-before +function MySelect() { + const value = useSignal(''); + + return ( +
+ +

You selected: {value}

+
); } // --repl-after @@ -259,7 +372,7 @@ Whilst bare inputs are useful and you can get far with them, often we'll see our To demonstrate, we'll create a new `
` element that contains two `` fields: one for a user's first name and one for their last name. We'll use the `onSubmit` event to listen for the form submission and update the state with the user's full name. - + ```jsx // --repl @@ -333,6 +446,44 @@ function FullNameForm() { render(, document.getElementById('app')); ``` +```jsx +// --repl +import { render } from 'preact'; +import { useSignal, useComputed } from '@preact/signals'; +import { Show } from '@preact/signals/utils'; +// --repl-before +function FullNameForm() { + const fullName = useSignal(''); + + const onSubmit = e => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fullName.value = formData.get('firstName') + ' ' + formData.get('lastName'); + e.currentTarget.reset(); // Clear the inputs to prepare for the next submission + }; + + const greeting = useComputed(() => `Hello ${fullName.value}`); + + return ( +
+ + + + + + {() =>

{greeting}

}
+
+ ); +} + +// --repl-after +render(, document.getElementById('app')); +``` +
> **Note**: Whilst it's quite common to see React & Preact forms that link every input field to component state, it's often unnecessary and can get unwieldy. As a very loose rule of thumb, you should prefer using `onSubmit` and the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) API in most cases, using component state only when you need to. This reduces the complexity of your components and may skip unnecessary rerenders. diff --git a/content/en/tutorial/09-error-handling.md b/content/en/tutorial/09-error-handling.md index 40110c4ae..e7b837882 100644 --- a/content/en/tutorial/09-error-handling.md +++ b/content/en/tutorial/09-error-handling.md @@ -1,7 +1,7 @@ --- title: Error Handling prev: /tutorial/08-keys -next: /tutorial/10-links +next: /tutorial/10-signals solvable: true --- diff --git a/content/en/tutorial/10-signals.md b/content/en/tutorial/10-signals.md new file mode 100644 index 000000000..bf9ddfefa --- /dev/null +++ b/content/en/tutorial/10-signals.md @@ -0,0 +1,308 @@ +--- +title: Signals +prev: /tutorial/09-error-handling +next: /tutorial/11-links +solvable: true +--- + +# Signals + +In chapter four, we saw how Preact manages state with the help of **hooks**. Hooks +rely upon re-rendering the component on state/prop change, and whilst this is a perfectly +usable set of APIs, we find many users struggle with modeling their components and using +this system without consistently running into some known pitfalls. + +**Signals** are another way to manage state in Preact. Signals can offer automatic fine-grained +updates, avoiding the need for dependency arrays and skipping full-component re-renders, as +well as a (subjectively) simpler flow for derived state. This helps ensure apps of all sizes are fast by default. +parts of the UI that actually uses them. A signal remembers its value and automatically tracks where it is used, so when the value +changes, only the parts that depend on it will update. + +### Creating a signal + +You can create a signal using the `signal()` function, Every signal has a `.value` property. +signals read it with `signal.value` and update the value. Signals gets updated only there's a change in a value, else it remains same. + +```js +import { useSignal } from '@preact/signals'; + +const count = useSignal(1); + +count.value = 1; //no updates +count.value = 2; //count updates +``` + +Signals can work with **arrays**, as you know whenever the value changes, the signal tells the specific part of the UI that uses it to update. +But if you change value inside an array directly, the signal won’t know whether the value is changed or not. +Instead, signals only update when they get a **new array**, so you need to create a copy of array with the updated values, which updates the UI correctly. + +```jsx +import { useSignal } from '@preact/signals'; + +const todos = useSignal(['Buy milk']); + +todos.value.push('Clean room'); //doesn't update +todos.value = [...todos, 'Study for exam']; //updates the UI +``` + +### Derived values with computed signals + +Sometimes you need a value that depends on other signals. For example, if you already have a first name and a last name, +now you want a full name which is derived from those first and last names. You can store it separately, +but then you have to update it every time when the first or last name changes. + +This is where `computed()` helps. A computed signal is like a signal that is automatically calculated from other signals I.e, it depends on values of other signal. +You will not set the value directly. Instead, you give a small function to it, and it will always run that function automatically whenever the original signals change. + +```jsx +import { useSignal, useComputed } from '@preact/signals'; + +const firstName = useSignal('John'); +const lastName = usesignal('Doe'); + +//fullName will always stay in sync with firstName & lastName +const fullName = useComputed(() => `${firstName.value} ${lastName.value}`); + +console.log(fullName); //John Doe + +//Update signal +firstName.value = 'Jane'; + +console.log(fullName); //Jane Doe (updated automatically) +``` + +## Side effects with effect() + +Signals where used to store and update values, but sometimes you also need to run some extra code whenever a signal changes. +Doing something in response to a signal change is called a side effect. For example, Imagine you have a signal that stores a number. +Every time the number changes, you also want to show an alert message. That extra action is not just updating the value it’s something extra that happens because the +value changed. This extra action is what we call a side effect. with effects you can also update the page title, save data, or call an API. + +That’s where `effect()` comes in. It runs a piece of code automatically whenever the signals inside it change. +You don’t have to call it yourself manually, it reacts to the signals it depends. + +```jsx +import { useSignal, useEffect } from '@preact/signals'; + +const name = useSignal('Alice'); + +useEffect(() => { + console.log(`Hello, ${name}!`); +}); + +name.value = 'Bob'; //"Hello, Bob!" +``` + +### Local state with Signals + +Local state is a state that belongs to **Single Component** and doesn't shared with whole app. +When creating signals inside a component you should use the hook versions `useSignal(initialValue)` works like `signal()`, +but it is memoized to ensure that the same signal instance is reused across re-renders. Similarly, useComputed(fn) works like computed(), +but it also memoized. In simple terms, `useSignal` is used for state that changes and only belongs to one component, +`useComputed` is used for creating derived values from those signals while the value changes. + +For example, let's consider a **Toggle component** with a button and a label, +where the button helps to switch the state of `const isOn = useSignal(false)` between `true` and `false`, +and `useComputed` is used to derive the state of label automatically based on value of `isOn`. + +```jsx +import { useSignal, useComputed } from '@preact/signals'; + +function Toggle() { + const isOn = useSignal(false); // local signal + const label = useComputed(() => (isOn.value ? 'ON' : 'OFF')); + + return ; +} +``` + +## Global state with Signals + +Global state is state which be accessed by **Multiple components**, to make a signal globally accesable, +create it outside using `signal(initialValue)`, which makes the state reusable anywhere, +and `computed(fn)`, which lets you define derived values which will update automatically when the signal changes. +The simplest way to use global state is to directly import and use the signal across multiple components. + +For example, `const theme = signal("light")` could be toggled in one component and read in another. +But this can become harder to manage in larger apps, there's a common way where the global state will wrap in a **Context**. +With context, you can create a function like `createAppState()` that stores your signals and computed values. +Then, you give this state to your whole app by wrapping it in `Context.Provider`. +By This way, any component inside the app can easily access the shared state without passing it through props. +Any component can then access this shared state using `useContext`. + +In short, using signal and computed directly as global state works well for simple apps, +but using context is helpful when many components in different parts of your app need to use the same global state. + +```jsx +import { signal, computed } from '@preact/signals'; + +export const counter = signal(0); +export const doubled = computed(() => counter.value * 2); + +function Increment() { + return ; +} + +function Display() { + return

Counter doubled: {doubled}

; +} +``` + +Here, `counter` and `doubled` are global signals that both components (Increment and Display) can use directly. + +```jsx +import { signal, computed } from '@preact/signals'; +import { createContext } from 'preact'; +import { useContext } from 'preact/hooks'; + +// Create a state object +function createAppState() { + const counter = signal(0); + const doubled = computed(() => counter.value * 2); + return { counter, doubled }; +} + +// Create a context +const AppState = createContext(); + +// Component 1: Increment button +function Increment() { + const state = useContext(AppState); // access shared state + return ; +} + +// Component 2: Display result +function Display() { + const state = useContext(AppState); // access shared state + return

Counter doubled: {state.doubled}

; +} + +// Main App +export function App() { + return ( + + + + + ); +} +``` + +Here, `counter` and `doubled` is created once inside `createAppState()` and passed to all components through `Context`, +so you don’t need to import global signals directly. + +## Try it! + +Let’s build a simple counter using `signal` that increases each time the button is clicked, and be sure to display the current count (`{count}`). + + +

🎉 Congratulations!

+

You learned how to use signals!

+
+ +```js:setup +useResult(function() { + var options = require('preact').options; + + var oe = options.event; + options.event = function(e) { + if (oe) oe.apply(this, arguments); + + if (e.currentTarget.localName !== 'button') return; + var root = e.currentTarget.parentNode.parentNode; + var text = root.innerText.match(/Count:\s*([\w.-]*)/i); + if (!text) return; + if (!text[1].match(/^-?\d+$/)) { + return console.warn( + "Tip: it looks like you're not rendering {count} anywhere." + ); + } + setTimeout(function() { + var text2 = root.innerText.match(/Count:\s*([\w.-]*)/i); + if (!text2) { + return console.warn('Tip: did you remember to render {count}?'); + } + if (text2[1] == text[1]) { + return console.warn( + 'Tip: remember to update the signal value using `.value`.' + ); + } + if (!text2[1].match(/^-?\d+$/)) { + return console.warn( + 'Tip: it looks like `count` is being set to something other than a number.' + ); + } + + if (Number(text2[1]) === Number(text[1]) + 1) { + solutionCtx.setSolved(true); + } + }, 10); + }; + + return function() { + options.event = oe; + }; +}, []); +``` + +```jsx:repl-initial +import { render } from 'preact'; +import { useSignal } from '@preact/signals'; + +function MyButton(props) { + return ( + + ); +} + +function App() { + const clicked = () => { + // increment the signal value here + }; + + return ( +
+

Count:

+ + Click me + +
+ ); +} + +render(, document.getElementById('app')); +``` + +```jsx:repl-final +import { render } from 'preact'; +import { useSignal } from '@preact/signals'; + +function MyButton(props) { + return ( + + ); +} + +function App() { + const count = useSignal(0); + + const clicked = () => { + count.value++; + }; + + return ( +
+

Count: {count}

+ + Click me + +
+ ); +} + +render(, document.getElementById('app')); +``` diff --git a/content/en/tutorial/10-links.md b/content/en/tutorial/11-links.md similarity index 96% rename from content/en/tutorial/10-links.md rename to content/en/tutorial/11-links.md index 15375184f..0e1781a5c 100644 --- a/content/en/tutorial/10-links.md +++ b/content/en/tutorial/11-links.md @@ -1,6 +1,6 @@ --- title: Congratulations! -prev: /tutorial/09-error-handling +prev: /tutorial/10-signals solvable: false --- @@ -14,6 +14,7 @@ Feel free to play around a bit more with the demo code. - [Learn more about class components](/guide/v10/components) - [Learn more about hooks](/guide/v10/hooks) +- [Learn more about signals](/guide/v10/signals) - [Create your own project](https://vite.new/preact) > **We want your feedback!** diff --git a/src/assets/_redirects b/src/assets/_redirects index 42c67e1db..66d9994ca 100644 --- a/src/assets/_redirects +++ b/src/assets/_redirects @@ -10,6 +10,7 @@ /guide/unit-testing-with-enzyme /guide/v10/unit-testing-with-enzyme /guide/progressive-web-apps /guide/v8/progressive-web-apps /guide/v10/tutorial /tutorial +/tutorial/10-links /tutorial/11-links # Return a smaller asset than the prerendered 404 page for failed "guess-and-check" translation fetches /content/* /content/en/404.json 404 diff --git a/src/config.json b/src/config.json index 594866798..e21070e27 100644 --- a/src/config.json +++ b/src/config.json @@ -1054,7 +1054,13 @@ } }, { - "path": "/tutorial/10-links", + "path": "/tutorial/10-signals", + "name": { + "en": "Signals" + } + }, + { + "path": "/tutorial/11-links", "name": { "en": "Congratulations!" }