0.13.5 • Published 6 months ago

@restate/core v0.13.5

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

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.

DevTools screenshot

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

0.13.1

6 months ago

0.13.2

6 months ago

0.13.3

6 months ago

0.13.4

6 months ago

0.13.5

6 months ago

0.12.0

2 years ago

0.11.0

4 years ago

0.10.0

4 years ago

0.9.4

4 years ago

0.9.3

4 years ago

0.9.2

4 years ago

0.9.0

4 years ago

0.9.1

4 years ago

0.8.10

5 years ago

0.8.9

5 years ago

0.8.8

5 years ago

0.8.7

5 years ago

0.8.6

5 years ago

0.8.5

5 years ago

0.8.4

5 years ago

0.8.3

5 years ago

0.8.2

5 years ago