jagdai
v4.0.2
Published
A React state and event management solution with almost no learning cost.
Downloads
29
Maintainers
Readme
Jagdai
A React state and event management solution with almost no refactoring cost.
What is Jagdai?
Jagdai (pronounced /ʒɑʁdɑj/
, it means "state" or "situation" in Kazakh) is a React state and event management solution with almost no refactoring cost.
- Almost no learning cost, easy to get started as long as you know how to use standard React Hook APIs.
- Refactoring is easy for projects without using a state management library, in order to achieve excellent performance.
- Simple and convenient cross-component event management approach.
- Excellent TypeScript type inference.
Try it on CodeSandbox
Installation
npm install jagdai
Usage
Create a store
First, create a store using the create
function provided in jagdai.
The create
function requires you to pass in a custom React Hook that defines the store.
This means you can use all types of React Hooks inside the function body.
You can define your store state using standard React APIs like useState
.
import { create } from 'jagdai'
import { useState } from 'react'
export const CounterStore = create(() => {
const [count, setCount] = useState(0)
return {
query: {
count,
},
}
})
However, the create
function has specific requirements for the return value of the hook you pass as its parameter:
- First, the return value must be an object.
- If you want to share cross-component state, you need to declare an object for its
query
field and bind the shared states one by one within it, indicating that components can query and subscribe to these states.
Mount the store-provider component on the component tree
The create function will return an object, which has a Provider
field that is a component.
Here, we named it to
CounterStore
based on the usage scenario of this store.
You need to mount the Provider
component onto a node of the React tree.
const CounterApp = () => {
return (
<CounterStore.Provider>
<Count />
<Controls />
</CounterStore.Provider>
)
}
Only in this way can its child components and even deeper-level components consume the state defined in the query
.
Query and consume state
The useQuery
field in the object returned by create
is a React Hook that allows you to query and consume the state declared in the query field of the store.
You must pass it a selector as an argument so that it can listen for changes to the subscribed states and decide whether to re-render the component.
For example, if you subscribe to the count field in the query
, the component will only re-render when count
updates.
const Count = () => {
const count = CounterStore.useQuery((query) => query.count)
return <div>{count}</div>
}
Sending commands to the store in components
In addition to declaring the query
field, you can also declare the command
field when defining a store, which represents the commands that components can send to the store.
For example, you can define a function in the store-definition's function body to update the state and bind it to the command
field.
export const CounterStore = create(() => {
const [count, setCount] = useState(0)
const increase = () => {
setCount(count + 1)
}
return {
query: {
count,
},
command: {
increase,
},
}
})
The create
return value contains a useCommand
field, which is also a Hook. You can use it to access the commands defined in the store.
const Controls = () => {
const { increase } = CounterStore.useCommand()
return <button onClick={increase}>+</button>
}
Whenever the button is clicked, increase
is executed and updates the count
in the store.
Then, components subscribed to count
through CounterStore.useQuery
are triggered to re-render and get the latest count
.
Why do I need command
-useCommand
?
With the pair of
query
anduseQuery
, I can share functions as a type of state with components. Why do I still needcommand
anduseCommand
?
The advantage of useCommand
is that the component will never re-render because of it
This is because the return value of useCommand
is constant. This means that the component will never re-render because the function field defined in command
points to a new function.
But don't worry: even though the return value of useCommand
is constant, the function obtained in the component will still call the latest function in the store when invoked.
Store-event
In the process of developing with React, having only state is sometimes not enough.
For example, there may be scenarios where illegal command parameters are entered, and the store-state does not change but the component needs to be aware of it.
TypeScript can only help us avoid illegal parameter types, but in some cases we can only check at runtime, such as when some scenarios require server confirmation.
Create a store-event
In addition to create
, Jagdai also provides the useEvent
hook for creating store events.
import { create, useEvent } from 'jagdai'
You can define a store-event using useEvent
similar to how you define a state using useState
. The return value of useEvent
is a function that can be used to dispatch this event.
Emitting event can carry a parameter. If your project environment is based on TypeScript, you can specify the type of its parameter.
export const CounterStore = create(() => {
const [count, setCount] = useState(0)
const onUpdateFail = useEvent<string>()
const update = (value: number) => {
setCount(value)
if (value === count) {
onUpdateFail(`The count is already ${count}`)
}
}
const increase = () => {
update(count + 1)
}
return {
query: {
count,
},
command: {
increase,
update,
},
event: {
onUpdateFail,
},
}
})
Here, based on the previous example, an onUpdateFail
event and an update
command are added.
The update
command updates count
to the number from the update
argument, and if count
and the update
argument are already equal, it emits an onUpdateFail
event (calling itself) to indicate that count
cannot be updated.
Similar to query
and command
, if you want to subscribe to this event in components, you need to bind the event to a field of the event
object.
The return value of create
has a useEvent
field, which is used to subscribe to events in the store from components.
Subscribing to store-event
The useEvent
field in the return value of create
requires two parameters:
- The event name, which must be one of the fields in the event object returned by the Hook parameter of
create
. - The event listener function.
const Controls = () => {
const { increase, update } = CounterStore.useCommand()
CounterStore.useEvent('onUpdateFail', (arg) => {
console.log(arg)
})
const [input, setInput] = useState(0)
return (
<>
<input
type="number"
onChange={(e) => {
setInput(parseInt(e.target.value, 10))
}}
/>
<button onClick={() => update(input)}>update to {input}</button>
<button onClick={increase}>+</button>
</>
)
}
If you want to subscribe to the event defined using useEvent
within the store, it's also very simple:
Just pass the listener function as a parameter when defining the event using useEvent
.
const onUpdateFail = useEvent((reason: string) => {
console.log(`Update failed, the reason is ${reason}`)
})
Query multiple states at once
Primitive types
const income = EmployeeStore.useQuery((query) => query.salary + query.bonus)
If either query.salary
or query.bonus
changes, it will trigger a re-render of the component.
Shallow comparison
When multiple states are combined into an object type returned, jagdai
provides useShallow
for determining updates through shallow comparison. The usage is as follows:
import { useShallow } from 'jagdai'
// ...
const [phone, email] = UserStore.useQuery(
useShallow((query) => [query.phone, query.email]),
)
If either query.phone
or query.email
changes, it will trigger a re-render of the component.
import { useShallow } from 'jagdai'
// ...
const { name, age } = UserStore.useQuery(
useShallow((query) => ({
name: `${query.firstName} ${query.lastName}`,
age: query.age,
})),
)
If any of query.firstName
, query.lastName
, or query.age changes
, it will trigger a re-render of the component.
By default, useQuery
uses strict equality comparison Object.is(old, new)
to detect changes.
In
jagdai
,useShallow
is provided to be used in combination withuseQuery
, employing a shallow comparison approach to decide whether to re-render.For more complex situations,
useQuery
offers a second optional argument, allowing you to customize the comparison function to override this default behavior.
Inspiration
Remesh: In Jagdai's design of APIs such as
query
,command
, andevent
, there are many traces of imitation of Remesh. If the conditions are appropriate, especially for developing large projects, I hope that the more powerful and advanced Remesh can become your preferred choice.Hox: Jagdai was born because there was a project that did not use a state management library, and because the frequent re-renders caused performance problems that were unacceptable, a low-cost refactoring solution was needed. If we had known about Hox at that time, Jagdai might not have been created. In addition, the problem of nesting the same Store component within the Store component also borrowed the solution from Hox.
Zustand: The selector-style API of Zustand inspired the design of
useQuery
.