@restate/core
v0.13.5
Published
_Restate_ is a predictable, easy to use, easy to integrate, typesafe state container for [React](https://reactjs.org/).
Downloads
51
Maintainers
Readme
Restate
Restate is a predictable, easy to use, easy to integrate, typesafe state container for React.
Restate follows the three principles of Redux:
- Single source of truth
- State is read only
- Changes are pure, but made in a convenient way using immer.js
Futhermore, Restate
- provides a nice React-Hook based API to read and update the state
- is using Typescript to make your application more robust and your development experience more enjoyable
- provide means to develop asynchronous state changes without the drama
- makes it easy integrate other components (server, logger, database, router,...) as the state is reactive.
- dev-tools
- easy to learn and easy to test
What it looks like...
const store = createStore({
state: {
name: "John Snow",
age: 32
}
})
const AppStoreProvider = createProvider(store);
const useAppState = createStateHook(AppStoreProvider);
const useNextAppState = createNextHook(AppStoreProvider);
const Name = () => {
const name = useAppState(state => state.user.name)
const next = useNextAppState(state => state.user)
function setName(nextName:string) {
next(user => user.name = nextName)
}
return <input value={name} onChange={e => setName(e.target.value)} />
}
Even the code above looks like JS, it is indeed Typescript. Go on StackBlitz and
make some changes to the state, for example change the property user.name
to user.firstName
. You will see how Typescript is
able to pick up those changes and gives you nice error messages.
Documentation
The documentation is also available on Netlify:https://restate.netlify.com/.
Installation
With NPM:
npm install @restate/core --save
or with YARN:
yarn add @restate/core
Store
To get started, you need to create a store:
import { createStore } from "@restate/core"
const store = createStore({
state: {
name: "John Snow",
age: 32
}
})
Try on StackBlitz!
Provider
To connect our store to the React component tree we need a Provider
:
import { createProvider } from "@restate/core"
const AppStoreProvider = createProvider(store) // to provide the store to react
const App: React.FC = () => (
<AppStoreProvider.Provider value={store}>
<Hello />
<Age />
<NameForm />
</AppStoreProvider.Provider>
)
Try on StackBlitz!
You can use multiple stores as well.
Read from the state
To read from the state Restate provides you AppStateHooks.
AppStateHooks hooks are use to
- select properties from your state
- do some basic computations / transformations
const store = createStore({
state: {
user: { name: "John Doe", age: 32 },
todos: 0
}
})
const AppStoreProvider = createProvider(store)
// create a `scoped state hook` (we select the `user` object)
const useUserState = createStateHook(AppStoreProvider, state => state.user)
const Hello = () => {
// select a property from the state
const name = useUserState(user => user.name)
// do some basic views/computations/transformations
const days = useUserState(user => user.age * 365)
return (
<h1>
Hello {name}, you are {days} days old
</h1>
)
}
Try on StackBlitz!
Change the state - using hooks
To change the state, we use a Next-Hook.
import { createNextHook } from "@restate/core"
// create a next-hook
const useNextAppState = createNextHook(AppStoreProvider)
The useNextAppState
hook takes a selector function to scope
the access to our state. In this example the scope is the user
object.
The useNextAppState
returns a customnext
function, which
can be use to change the user
object:
const NameForm = () => {
const name = useAppState(state => state.user.name)
// Creates a `next` function to change the user
const next = useNextAppState(state => state.user)
function setName(nextName: string) {
// The next function provides the current user object as parameter, which we can modify.
next(user => (user.name = nextName))
}
return <input value={name} onChange={e => setName(e.target.value)} />
}
Try on StackBlitz!
Change the state - using actions
Another way to modify your state are Actions.
Actions are forged in an ActionFactory. The ActionFactory is a function that
receives - among other things - the next()
function to update the store.
An ActionFactory returns a object that holds the all the actions to change the state. Think about actions as "member functions" of your state.
Actions can be asynchronous.
// Action factory
const userActionsFactory = ({ next }: ActionFactoryProps<User>) => ({
incrementAge() {
next(user => user.age++)
},
decrementAge() {
next(user => user.age--)
},
async fetchData(userId: string) {
const data = await serverFetchUserData(userId)
next(user => (user.data = data))
}
})
The ActionFactory is hooked into React
using the createActionsHook
:
const useUserActions = createActionsHook(
AppStoreProvider,
state => state.user,
userActionsFactory
)
const Age = () => {
const userActions = useUserActions()
return (
<div>
<button onClick={userActions.incrementAge}>+</button>
<button onClick={userActions.decrementAge}>-</button>
</div>
)
}
Try on StackBlitz
Change the state - using store.next()
Outside of your component tree you can change the store like this:
store.next(state => {
state.user.name = "John"
})
Middleware
Middleware are small synchronous functions which intercept state updates. Middleware functions receiving the currentState
as well as the nextState
.
They can change the nextState
, if required. If a middleware throws an exception, the state update will be canceled.
Take the ageValidator
middleware for example.
It ensures, that the user.age
property never gets negative.
// Ensures the age will never be < 0
const ageValidator: Middleware<State> = ({ nextState, currentState }) => {
nextState.age =
nextState.user.age < 0 ? currentState.user.age : nextState.user.age
}
const store = createStore({
state: {
name: "John Snow",
age: 32
},
middleware: [ageValidator]
})
store.next(s => (s.user.age = -1)) // will be intercepted by the ageValidator middleware.
Try on StackBlitz
Connectors
Connectors "glue" your store to other parts of the application, for example to your server, database, ...
Connectors can
- observer the state and react to state changes using the
store.state$
observable - change the state using the
store.next()
function - listen to events dispatched on the
state.messageBus$
observable. The messages are similar to redux actions.
Observe store.state$
Here is an very simple logger example, that observes the state and logs all state changes:
function connectLogger(store: RxStore<any>) {
store.state$.subscribe(nextState => {
console.log("STATE:", JSON.stringify(nextState.payload))
})
}
connectLogger(store)
Try on StackBlitz
Change the state with store.next()
Another example of a connector could be a socket.io adapter, that receives chat messages from a server and adds them to the application state:
function connectSocket(store: RxStore<any>) {
socket.on("chat message", msg => {
store.next(state => {
state.messages.push(msg)
})
})
}
connectSocket(store)
Listen to events
Connectors can also receive messages from the application - redux style.
Here is a simple UNDO example. The connector records the history of the app state using the store.state$
observable.
The connector also listens to the UNDO
events by subscribing the store.messageBus$
.
If it receives the UNDO
event, it rewinds the state history by one step.
const history = []
// record state history
store.state$.subscribe(nextState => {
history.push(nextState);
})
// listen to UNDO events
store.messageBus$.subscribe( msg => {
if(msg.type === 'UNDO' && history.length > 1) {
history.pop() // remove current state
const prevState = history.pop();
store.next(prevState);
}
})
}
connectUndo(store);
The application uses createDispatchHook
to create a dispatch hook. With the dispatch hook, a component can dispatch an UNDO
event, like so:
const useDispatch = createDispatchHook(AppStoreProvider)
function useUndo() {
const dispatch = useDispatch()
return () => dispatch({ type: "UNDO" })
}
const UndoButton = () => {
const undo = useUndo()
return <button onClick={undo}>UNDO</button>
}
Try the UNDO example on StackBlitz!
DevTools
restate
uses the
excellent ReduxDevTools to provide power-ups for your development workflow.
Installation
Go and get the ReduxDevTools for your browser:
- Google Chrome
- Firefox
Then install the @restate/dev-tools
yarn add @restate/dev-tools
Usage
import { connectDevTools } from "@restate/dev-tools"
const store = createStore({
state: {
name: "John Snow",
age: 32
},
options: {
storeName: "MY APP STORE" // <-- will show up in the instance selector
}
})
connectDevTools(store)
License
MIT