0.2.0 • Published 5 years ago

@francisco.ruiz/global-state v0.2.0

Weekly downloads
3
License
MIT
Repository
github
Last release
5 years ago

Global State

Lightweight package to handle the global state of our React-based-applications via the new React Context API.

Table of Contents

Motivation

Although right now we have very little needs regarding this topic (we moslty work on business logic-focused products), we have a common need for the reasons we'll review in the sections below.

Current issues

  • At this moment, we have one single way to share data ("global state of our business logic") between different components, but it's performed within the Domain through the streamify decorator.
  • There are some data that we don't want to mix with our business logic: information from the browser, user interface or even user preferences.
  • We use a lot the ugly pattern of passing down data throughout the whole tree, component by componet... annoying right?

Why React Context

  • It's basically React, no any other 3rd-party libraries needed.
  • Ease to use and very well documented.
  • Very little boilerplate (almost none) in order to start using it. So different regarding other 3rd-party libraries like Redux.
  • We avoid unnecessary renders by using a context provider that is a PureComponent and handles the "global state" locally, so that any other unneeded prop is passed down, just consumers will get the new updated values.

API Reference

Regarding this point, it's important to say the final implementation is completely based in a 3rd-party utility: https://github.com/dai-shi/react-context-global-state. That means at this moment it fits the most needs we have an also fulfill all my expectations I've had in this project. However, it doesn't mean we can adapt it by modifying the output of our library, or even create our own rewritten library. At the end of the day, the main ideas stand within that code.

Top-Level Exports

  • createGlobalState, whose input is a plain object, and for each property a context will be created.
    • input: {Object} initialState
    • output: {Object} {StateProvider, StateConsumer, stateItemConsumers, stateItemUpdaters}
  • withGlobalStateFactory, a small utility to, once passed a needed GlobalStateConsumer, wrap any component setting a new prop with the global state value required.

Example of Usage

Install

npm install @francisco.ruiz/global-state --save

Create your own state wrapper

I swear it's the hardest part 😛 Let's create it in the root directory and name it GlobalState:

// GlobalState.js
import {createGlobalState, withGlobalStateFactory} from '@francisco.ruiz/global-state'

// Set your initial state.
const initialState = {
  theme: 'dark',
  language: 'en',
  notifications: []
}

// Create your global state.
const {
  StateProvider: GlobalStateProvider,
  StateConsumer: GlobalStateConsumer,
  stateItemUpdaters
} = createGlobalState(initialState)

// Set your actions in order to update your global state.
const actions = {
  toggleTheme: () => {
    const update = stateItemUpdaters.theme

    update(theme => (theme === 'light' ? 'dark' : 'light'))
  },
  updateLanguage: language => {
    const update = stateItemUpdaters.language

    update(() => language)
  },
  pushNotification: notification => {
    const update = stateItemUpdaters.notifications

    update(notifications => [...notifications, notification])
  }
}

// Generate your own HOC in order to consume easily the global state by any context.
// Call it by passing first the context and then the component: ´withGlobalState('theme', Component)´
const withGlobalState = withGlobalStateFactory(GlobalStateConsumer)

// Then export it all.
export {GlobalStateProvider, GlobalStateConsumer, withGlobalState, actions}

Provide your app

Do it where you render your app, let's say in your main file:

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter} from 'react-router-dom'
import {GlobalStateProvider} from './GlobalState'
import App from './App'
import './index.scss'

ReactDOM.render(
  <GlobalStateProvider>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </GlobalStateProvider>,
  document.getElementById('root')
)

Consume the global state

You can do it in the component you want, but let's watch an example within your app component:

// App.js
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import {withGlobalState, actions} from './GlobalState'

function App({title, theme}) {
  const isDark = theme === 'dark'

  return (
    <div className={classNames('mt-App', {
      'is-dark': isDark
    })}>
      <h1 className="mt-App-title">{title}</h1>
      <div className="mt-App-toggleTheme">
        Toggle theme
        <input
          type="checkbox"
          checked={isDark}
          onChange={actions.toggleTheme}
        />
      </div>
    </div>
  )
}

App.propTypes = {
  title: PropTypes.string,
  theme: PropTypes.string
}

App.defaultProps = {
  title: 'Hello World!'
}

export default withGlobalState('theme', App)

And now, enjoy! 🎉

Things to be considered

  • If you're using a BrowserRouter wrapping your app you'll have to place your GlobalStateProvider into an upper level because it uses a PureComponent and it won't let the BrowserRouter to propagate the changes to the app components.
  • Within the context of Schibsted Spain, we recommend to wrap the global state and publish it as a npm package.
  • On the global state creation, try to separate the initial state from the actions and the creation itself in different files.

Demo

You can see it working in the project demo I've made for the Front End Enabler test: https://francisco-ruiz-enabler-test.now.sh/

In this project you can toggle the theme between their light and dark modes and also navigate through the main funnel (list and detail) while the browser scroll is kept at its last position on the same page type.

Documentation

Next steps

  • Split actions by context.
  • Accept to send multiple global state contexts at a time when calling withGlobalState, for instance: withGlobalState(['theme', 'language'], Component).
  • Generate specific consumer HOCs for each context, e.g.: withTheme, withBrowser.
  • Support decorators and even React Hooks 😄