|
| 1 | +# Reactivity |
| 2 | + |
| 3 | +## Signals |
| 4 | + |
| 5 | +Signals are the glue that hold the library together. They often are invisible but interact in very powerful ways that you get more familiar with Solid they unlock a lot of potential. |
| 6 | + |
| 7 | +Signals are a simple primitive that contain values that change over time. With Signals you can track sorts of changes from various sources in your applications. Solid's State object is built from a Proxy over a tree of Signals. You can update a Signal manually or from any Async source. |
| 8 | + |
| 9 | +```js |
| 10 | +import { createSignal, onCleanup } from "solid-js"; |
| 11 | + |
| 12 | +function useTick(delay) { |
| 13 | + const [getCount, setCount] = createSignal(0), |
| 14 | + handle = setInterval(() => setCount(getCount() + 1), delay); |
| 15 | + onCleanup(() => clearInterval(handle)); |
| 16 | + return getCount; |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +## Accessors Reactive Scope |
| 21 | + |
| 22 | +Signals are special functions that when executed return their value. In addition they are trackable when executed under a reactive scope. This means that when their value read (executed) the currently executing reactive scope is now subscribed to the Signal and will re-execute whenever the Signal is updated. |
| 23 | + |
| 24 | +This mechanism is based on executing function scope so Signals reads can be composed and nested as many levels as desired. By wrapping a Signal read in a thunk `() => signal()` you have effectively created a higher-order signal that can be tracked as well. These accessors are just functions that can be tracked and return a value. No additional primitive or method is needed for them to work as Signals in their own right. However, you need another primitive to make Signals reactive: |
| 25 | + |
| 26 | +## Computations |
| 27 | + |
| 28 | +An computation is calculation over a function execution that automatically dynamically tracks any child signals. A computation goes through a cycle on execution where it releases its previous execution's dependencies, then executes grabbing the current dependencies. |
| 29 | + |
| 30 | +There are 2 main computations used by Solid: Effects which produce side effects, and Memos which are pure and designed to cache values until their reactivity forces re-evaluation. |
| 31 | + |
| 32 | +```js |
| 33 | +import { createSignal, createEffect, createMemo } from "solid-js"; |
| 34 | + |
| 35 | +const [count, setCount] = createSignal(1), |
| 36 | + doubleCount = createMemo(() => count() / 2) |
| 37 | +createEffect(() => console.log(doubleCount())); |
| 38 | +setCount(count() + 1); |
| 39 | + |
| 40 | +// 2 |
| 41 | +// 4 |
| 42 | +``` |
| 43 | + |
| 44 | +Keep in mind memos are only necessary if you wish to prevent re-evaluation when the value is pulled. Useful for expensive operations like DOM Node creation. Any example with a memo could also just be a function and effectively be the same without caching. |
| 45 | + |
| 46 | +```js |
| 47 | +import { createSignal, createEffect } from "solid-js"; |
| 48 | + |
| 49 | +const [count, setCount] = createSignal(1), |
| 50 | + doubleCount = () => count() / 2 |
| 51 | +// No memo still works |
| 52 | +createEffect(() => console.log(doubleCount())); |
| 53 | +setCount(count() + 1); |
| 54 | + |
| 55 | +// 2 |
| 56 | +// 4 |
| 57 | +``` |
| 58 | + |
| 59 | +Memos also pass the previous value on each execution. This is useful for reducing operations (obligatory Redux in a couple lines example): |
| 60 | + |
| 61 | +```js |
| 62 | +const reducer = (state, action = {}) => { |
| 63 | + switch (action.type) { |
| 64 | + case "LIST/ADD": |
| 65 | + return { ...state, list: [...state.list, action.payload] }; |
| 66 | + default: |
| 67 | + return state; |
| 68 | + } |
| 69 | +}; |
| 70 | + |
| 71 | +// redux |
| 72 | +const [getAction, dispatch] = createSignal(), |
| 73 | + getStore = createMemo(state => reducer(state, getAction()), { list: [] }); |
| 74 | + |
| 75 | +// subscribe and dispatch |
| 76 | +createEffect(() => console.log(getStore().list)); |
| 77 | +dispatch({ type: "LIST/ADD", payload: { id: 1, title: "New Value" } }); |
| 78 | +``` |
| 79 | + |
| 80 | +That being said there are plenty of reasons to use actual Redux. |
| 81 | + |
| 82 | +## Rendering with Reactivity |
| 83 | + |
| 84 | +Solid makes use of it's reactive lifecycle to render the DOM. Creating and updating the DOM are seen as side effects of the reactive system and the tree is constructed by nesting Computations wrapping each binding and dynamic insert with one. You can view its execution like a stack where only the top-most computation is tracking at a given time, and so is the only one tracking the reactive change. Since attributes and inserts are tracked separately from the parent scope responsible for rendering a Component in the first place, updates to attributes or downstream nodes do not require the parent to re-evaluate. If the parent ever were it would wipe out all the children and start again. However the only thing that would make that happen is if something upstream changed, like the condition that made it render in the first place. In so when in the synchronous execution path you are always under a reactive context even if it is not tracking (like a `root`). |
| 85 | + |
| 86 | +## Cleanup |
| 87 | + |
| 88 | +While Solid does not have Component lifecyles in the traditional sense, it still needs to handle cleaning up subscriptions. The way Solid works is that each nested computation is owned by it's parent reactive scope. In so all commputations must be created as part of a root. This detail is generally taken care of for you as the `render` method contains a `createRoot` call. But it can be called directly for cases where it makes sense. |
| 89 | + |
| 90 | +Once inside a scope whenever the scope is re-evaluated or disposed of itself, all children computations will be disposed. In addition you can register a `onCleanup` method that will execute as part of this disposal cycle. |
| 91 | + |
| 92 | +Note: _Solid's graph is synchronously executed so any starting point that isn't caused by a reactive update (perhaps an asynchronous entry) should start from its own root. There are other ways to handle asynchronicity as shown in the [Suspense Docs](./supense.md) |
| 93 | + |
| 94 | +## Composition |
| 95 | + |
| 96 | +State and Signals combine wonderfully as wrapping a state selector in a function instantly makes it reactive accessor. They encourage composing more sophisticated patterns to fit developer need. |
| 97 | + |
| 98 | +```js |
| 99 | +// deep reconciled immutable reducer |
| 100 | +const useReducer = (reducer, init) => { |
| 101 | + const [state, setState] = createState(init), |
| 102 | + [getAction, dispatch] = createSignal(); |
| 103 | + createDependentEffect( |
| 104 | + (prevState = init) => { |
| 105 | + let action, next; |
| 106 | + if (!(action = getAction())) return prevState; |
| 107 | + next = reducer(prevState, action); |
| 108 | + setState(reconcile(next)); |
| 109 | + return next; |
| 110 | + }, |
| 111 | + [getAction] |
| 112 | + ); |
| 113 | + return [state, dispatch]; |
| 114 | +}; |
| 115 | +``` |
| 116 | + |
| 117 | +## Operators |
| 118 | + |
| 119 | +Solid provides a couple simple operators to help construct more complicated behaviors. They work both as standalone and curried Functional Programming form, where they return a function that takes the input accessor. They are not computations themselves and are designed to be passed into a computation. The possibilities of operators are endless. Solid only ships with a base array mapping one: |
| 120 | + |
| 121 | +### `mapArray(() => any[], iterator: (item, index) => any, options: { fallback: () => any }): () => any[]` |
| 122 | + |
| 123 | +### `mapArray(iterator: (item, index) => any, options: { fallback: () => any }): (signal) => () => any[]` |
| 124 | + |
| 125 | +The `solid-rx` package contains more operators that can be used with Solid. |
0 commit comments