Core Concepts
Overview
StateX lets you create a react based application with a flexible state management solution. Atoms are units of state that components can subscribe to. Selectors transform this state either synchronously or asynchronously.
Path
( string | number )[]
Path points to a unit of state. You can use path to read or write any node in your state tree. Path can be dynamic and constructed on the fly. StateX (aka State) tree is immutable. When an atom is updated, each subscribed component is re-rendered with the new value.
Demo
Atoms
Atoms are units of state at a specific path. They're updateable and subscribable: when an atom is updated, each subscribed component is re-rendered with the new value. They can be created at runtime, too. Atoms can be used in place of React local component state. If the same atom is used from multiple components, all those components share the same unit of state.
Atoms are created using the atom() function:
Atoms need a unique path, which is used for building the state JSON object with each segment in the path becoming the object key. Nested objects can be created with mulitiple segments in the path. It is an error for two atoms to have the same path, so make sure they're globally unique. Like React component state, they also have a default value.
To read and write an atom from a component, we use a hook called useStateX.
It's just like React's useState
, but now the state can be shared between components:
Clicking on the button will increase the font size of the button by one. But now some other component can also use the same font size:
Demo
Selectors
A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated. Components can subscribe to selectors just like atoms, and will then be re-rendered when the selectors change.
Selectors are used to calculate derived data that is based on state. This lets us avoid redundant state, usually obviating the need for reducers to keep state in sync and valid. Instead, a minimal set of state is stored in atoms, while everything else is efficiently computed as a function of that minimal state. Since selectors keep track of what components need them and what state they depend on, they make this functional approach more efficient.
From the point of view of components, selectors and atoms have the same interface and can therefore be substituted for one another.
Selectors are defined using the selector() function:
The get
property is the function that is to be computed. It can access
the value of any path, atoms and other selectors using the get
argument
passed to it. Whenever it accesses another path, atom or selector, a
dependency relationship is created such that updating the other atom or
selector will cause this one to be recomputed.
In this ['fontSizeLabelStateX']
example, the selector has one dependency:
the ['fontSizeStateX']
atom. Conceptually, the ['fontSizeLabelStateX']
selector behaves like a pure function that takes a ['fontSizeStateX']
as
input and returns a formatted font size label as output.
Selectors can be read using useStateXValue(), which takes a, path, atom
or selector as an argument and returns the corresponding value. We don't
use the useStateX() as the fontSizeLabelStateX
selector is not
writeable (see the selector API reference
for more information on writeable selectors):
Clicking on the button now does two things: it increases the font size of the button while also updating the font size label to reflect the current font size.
Demo
Actions
An action is a reusable pure function that has access to the StateX state
and an ability to update the StateX state or invoke other actions.
Unlike selector, actions will not subscribe to any changes to the
atoms
or selectors
it reads.
Actions are used to keep multiple state updates in sync with some common, reusable logic. This lets us avoid duplicate code and disconnected state. Since action have full access to the state, it can perform state updates asyncronusly as well. With actions, you can modularize your business logic outside the component, thereby making the components lite weight.
Actions are defined using the action() function:
The function passed to action is to be executed on demand. It can access
the value of any path, atoms, selectors using the get
argument
passed to it. It can update the state using the set
argument and call any
other action using the call
argument.
In this activateNodeAction
example, the action updates two atom states to
mark a given nodeId as active and also set a specific tab as active.
Actions can be accessed using useStateXAction(), which takes the action as an argument and returns a callback function that can be invoked on user action.