1.0.0-beta.10 • Published 2 years ago

react-inner-store v1.0.0-beta.10

Weekly downloads
-
License
BSD-3-Clause
Repository
github
Last release
2 years ago

react-inner-store

A lightweight and performant React state management.

Get started

First, install the package:

# with yarn
yarn add react-inner-store

# with npm
npm install react-inner-store

And use it in your project:

import React from 'react'
import { InnerStore, useInnerState } from 'react-inner-store'

type State = {
  username: InnerStore<string>
  count: InnerStore<number>
}

const Username: React.VFC<{
  store: InnerStore<string>
}> = React.memo(({ store }) => {
  const [username, setUsername] = useInnerState(store)

  return (
    <input
      type="text"
      value={username}
      onChange={e => setUsername(e.target.value)}
    />
  )
})

const Counter: React.VFC<{
  store: InnerStore<number>
}> = React.memo(({ store }) => {
  const [count, setCount] = useInnerState(store)

  return (
    <div>
      <button onClick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
})

const App: React.VFC<{
  state: State
}> = React.memo(({ state }) => (
  <div>
    <h1>
      The app component will never re-render due to neither state.count nor
      state.username change
    </h1>

    <Username store={state.username} />
    <Counter store={state.count} />

    <button
      onClick={() => {
        const username = state.username.getState()
        const count = state.count.getState()

        console.log(`User "${username}" get ${count} score.`)
      }}
    >
      Submit
    </button>
  </div>
))

ReactDOM.render(
  <App
    state={{
      username: InnerStore.of(''),
      count: InnerStore.of(0)
    }}
  />,
  document.getElementById('root')
)

API

A core concept of the library is the InnerStore class. It is a mutable wrapper around a value that allows to prevent unnecessary re-renders. The class provides an API to get and set the value, and to observe changes. There are hooks built on top of the API for convenient usage in React components.

InnerStore.of

InnerStore.of<T>(value: T): InnerStore<T>

A static method that creates a new InnerStore instance. The instance is mutable so once created it should be used for all future operations.

type SignInFormState = {
  isSubmitting: boolean
  username: InnerStore<string>
  password: InnerStore<string>
  rememberMe: InnerStore<boolean>
}

const signInFormStore = InnerStore.of<SignInFormState>({
  isSubmitting: false,
  username: InnerStore.of(''),
  password: InnerStore.of(''),
  rememberMe: InnerStore.of(false)
})

InnerStore#key

InnerStore<T>#key: string

Each InnerStore instance has a unique key. This key is used internally for useInnerWatch but can be used as the React key property.

const Toggles: React.VFC<{
  options: Array<InnerStore<boolean>>
}> = ({ options }) => (
  <>
    {options.map(option => (
      <Toggle key={option.key} store={option} />
    ))}
  </>
)

InnerStore#clone

InnerStore<T>#clone(transform?: (value: T) => T): InnerStore<T>

An InnerStore instance's method that creates a new InnerStore instance with the same value.

  • [transform] is an optional function that will be applied to the current value before cloning. It might be handy when cloning a InnerStore instance that contains mutable values (e.g. InnerStore).
const signInFormStoreClone = signInFormStore.clone(
  ({ isSubmitting, username, password, rememberMe }) => ({
    isSubmitting,
    username: username.clone(),
    password: password.clone(),
    rememberMe: rememberMe.clone()
  })
)

InnerStore#getState

InnerStore<T>#getState(): T
InnerStore<T>#getState<R>(transform: (value: T) => R): R

An InnerStore instance's method that returns the current value.

  • [transform] is an optional function that will be applied to the current value before returning.
const plainSignInState = signInFormStore.getState(
  ({ isSubmitting, username, password, rememberMe }) => ({
    isSubmitting,
    username: username.getState(),
    password: password.getState(),
    rememberMe: rememberMe.getState()
  })
)

InnerStore#setState

InnerStore<T>#setState(
  valueOrTransform: React.SetStateAction<T>,
  compare?: Compare<T>
): void

An InnerStore instance's method that sets the value. Each time when the value is changed all of the store's listeners passed via InnerStore#subscribe are called.

  • valueOrTransform is either the new value or a function that will be applied to the current value before setting.
  • [compare] is an optional Compare function with strict check (===) by default. If the new value is comparably equal to the current value neither the value is set nor the listeners are called.

💬 The method returns void to emphasize that InnerStore instances are mutable.

const onSubmit = () => {
  signInFormStore.update(state => {
    // reset password field
    state.password.setState('')

    return {
      ...state,
      isSubmitting: true
    }
  })
}

InnerStore#subscribe

InnerStore<T>#subscribe(listener: VoidFunction): VoidFunction

An InnerStore instance's method that subscribes to the store's value changes caused by InnerStore#setState calls. Returns a cleanup function that can be used to unsubscribe the listener.

  • listener is a function that will be called on store updates.
const UsernameInput: React.VFC<{
  store: InnerStore<string>
}> = React.memo(({ store }) => {
  const [username, setUsername] = React.useState(store.getState())

  React.useEffect(() => {
    // the listener is called on every store.setState() call across the app
    return store.subscribe(() => setUsername(store.getState()))
  }, [store])

  return (
    <input
      type="text"
      value={username}
      // all store.subscribe across the app will call their listeners
      onChange={e => store.setState(e.target.value)}
    />
  )
})

useInnerState

function useInnerState<T>(
  store: InnerStore<T>,
  compare?: Compare<T>
): [T, React.Dispatch<React.SetStateAction<T>>]

function useInnerState<T>(
  store: null | undefined | InnerStore<T>,
  compare?: Compare<T>
): [null | undefined | T, React.Dispatch<React.SetStateAction<T>>]

A hook that is similar to React.useState but for InnerStore instances. It subscribes to the store changes and returns the current value and a function to set the value.

  • store is an InnerStore instance but can be null or undefined as a bypass when there is no need to subscribe to the store's changes.
  • [compare] is an optional Compare function with strict check (===) by default. The store won't update if the new value is comparably equal to the current value.
const UsernameInput: React.VFC<{
  store: InnerStore<string>
}> = React.memo(({ store }) => {
  const [username, setUsername] = useInnerState(store)

  return (
    <input
      type="text"
      value={username}
      onChange={e => setUsername(e.target.value)}
    />
  )
})

useInnerWatch

function useInnerWatch<T>(watcher: () => T, compare?: Compare<T>): T

A hook that subscribes to all InnerStore#getState execution involved in the watcher call. Due to the mutable nature of InnerStore instances a parent component won't be re-rendered when a child's InnerStore value is changed. The hook gives a way to watch after deep changes in the store's values and trigger a re-render when the returning value is changed.

type State = {
  count: InnerStore<number>
}

const App: React.VFC<{
  state: State
}> = React.memo(({ state }) => {
  // the component will re-render once the `count` is greater than 5
  // and once the `count` is less or equal to 5
  const isMoreThanFive = useInnerWatch(() => state.count.getState() > 5)

  return (
    <div>
      <Counter store={state.count} />

      {isMoreThanFive && <p>You did it!</p>}
    </div>
  )
})

💡 It is recommended to memoize the watcher function for better performance.

useInnerReducer

function useInnerReducer<A, T>(
  store: InnerStore<T>,
  reducer: (state: T, action: A) => T,
  compare?: Compare<T>
): [T, React.Dispatch<A>]

function useInnerReducer<A, T>(
  store: null | undefined | InnerStore<T>,
  reducer: (state: T, action: A) => T,
  compare?: Compare<T>
): [null | undefined | T, React.Dispatch<A>]

A hook that is similar to React.useReducer but for InnerStore instances. It subscribes to the store changes and returns the current value and a function to dispatch an action.

  • store is an InnerStore instance but can be null or undefined as a bypass when there is no need to subscribe to the store's changes.
  • reducer is a function that transforms the current value and the dispatched action into the new value.
  • [compare] is an optional Compare function with strict check (===) by default. The store won't update if the new value is comparably equal to the current value.
type CounterAction = { type: 'INCREMENT' } | { type: 'DECREMENT' }

const counterReducer = (state: number, action: CounterAction) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1

    case 'DECREMENT':
      return state - 1
  }
}

const Counter: React.VFC<{
  store: InnerStore<number>
}> = React.memo(({ store }) => {
  const [count, dispatch] = useInnerReducer(store, counterReducer)

  return (
    <div>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
    </div>
  )
})

useInnerUpdate

function useInnerUpdate<T>(
  store: null | undefined | InnerStore<T>,
  compare?: Compare<T>
): React.Dispatch<React.SetStateAction<T>>

A hooks that returns a function to update the store's value. Might be useful when you need a way to update the store's value without subscribing to its changes.

  • store is an InnerStore instance but can be null or undefined as a bypass when a store might be not defined.
  • [compare] is an optional Compare function with strict check (===) by default. The store won't update if the new value is comparably equal to the current value.
type State = {
  count: InnerStore<number>
}

const App: React.VFC<{
  state: State
}> = React.memo(({ state }) => {
  // the component won't re-render on the count value change
  const setCount = useInnerUpdate(state.count)

  return (
    <div>
      <Counter store={state.count} />

      <button onClick={() => setCount(0)>Reset count</button>
    </div>
  )
})

Compare

type Compare<T> = (prev: T, next: T) => boolean

A function that compares two values and returns true if they are equal. Depending on the type of the values it might be more efficient to use a custom compare function such as shallow-equal or deep-equal.

ExtractInnerState

A helper type that shallowly extracts value type from InnerStore:

type SimpleStore = InnerStore<number>
// ExtractInnerState<SimpleStore> === number

type ArrayStore = InnerStore<Array<string>>
// ExtractInnerState<ArrayStore> === Array<string>

type ShapeStore = InnerStore<{
  name: string
  age: number
}>
// ExtractInnerState<ShapeStore> === {
//   name: string
//   age: number
// }

type ShapeOfStores = InnerStore<{
  name: InnerStore<string>
  age: InnerStore<number>
}>
// ExtractInnerState<ShapeStore> === {
//   name: InnerStore<string>
//   age: InnerStore<number>
// }

DeepExtractInnerState

A helper that deeply extracts value type from InnerStore:

type ShapeOfStores = InnerStore<{
  name: InnerStore<string>
  age: InnerStore<number>
}>
// DeepExtractInnerState<ShapeStore> === {
//   name: string
//   age: number
// }

type ArrayOfStores = InnerStore<Array<InnerStore<boolean>>>
// DeepExtractInnerState<ArrayOfStores> === Array<boolean>
1.0.0-beta.11

2 years ago

1.0.0-beta.10

2 years ago

1.0.0-beta.9

2 years ago

1.0.0-beta.6

2 years ago

1.0.0-beta.7

2 years ago

1.0.0-beta.8

2 years ago

1.0.0-beta.5

2 years ago

1.0.0-beta.4

2 years ago

1.0.0-beta.3

2 years ago

1.0.0-beta.2

2 years ago

1.0.0-beta.1

2 years ago

1.0.0-beta.0

2 years ago