Sawtaytoes
10/7/2018 - 11:56 AM

Redux-like State Components

Full example of using Redux-like State Components with a form validator and loading state component.

import { Children, Component, createElement } from 'react'

const throwError = () => {
  throw new Error(
    'Missing either `children`, `component`, or `render` prop.',
  )
}

class StateSubscriber extends Component {
  constructor(props) {
    super(props)

    const { subscribe } = props

    this.unsubscribe = (
      subscribe(
        this
        .setState
        .bind(this)
      )
    )
  }

  componentWillUnmount() {
    this.unsubscribe
    && this.unsubscribe()
  }

  render() {
    const {
      children,
      component,
      render,
      ...props
    } = this.props

    const childProps = {
      ...props,
      ...this.state,
    }

    return (
      component
      ? createElement(component, childProps)
      : (
        render
        ? render(childProps)
        : (
          children
          ? (
            typeof children === 'function'
            ? children(childProps)
            : Children.only(children)
          )
          : throwError()
        )
      )
    )
  }
}

const createStateSubscriber = (
  renderProps,
) => (
  props,
) => (
  createElement(
    StateSubscriber,
    {
      ...props,
      ...renderProps,
    },
  )
)

export default createStateSubscriber
import { Children, Component, createElement } from 'react'

const throwError = () => {
  throw new Error(
    'Missing either `children`, `component`, or `render` prop.',
  )
}

class StateSubscriber extends Component {
  constructor(props) {
    super(props)

    const { subscribe } = props

    this.unsubscribe = (
      subscribe(
        this
        .setState
        .bind(this)
      )
    )
  }

  componentWillUnmount() {
    this.unsubscribe
    && this.unsubscribe()
  }

  render() {
    const {
      children,
      component,
      render,
      ...props
    } = this.props

    const childProps = {
      ...props,
      ...this.state,
    }

    return (
      component
      ? createElement(component, childProps)
      : (
        render
        ? render(childProps)
        : (
          children
          ? (
            typeof children === 'function'
            ? children(childProps)
            : Children.only(children)
          )
          : throwError()
        )
      )
    )
  }
}

const createStateSubscriber = (
  dispatchActions,
) => (
  props,
) => (
  createElement(
    StateSubscriber,
    {
      ...props,
      ...dispatchActions,
    },
  )
)

export default createStateSubscriber
import React from 'react'

import FormValidationState from './FormValidationState'

const onSubmit = (
  validator,
) => (
  event,
) => {
  event
  .preventDefault()

  validator({
    fieldNames: [
      'search',
    ],
    formElement: (
      event
      .target
    ),
  })
}

const Form = ({
  errorMessage,
}) => (
  <FormValidationState>
    {({
      isValid,
      validateFields,
    }) => (
      <form onSubmit={onSubmit(validateFields)}>
        {
          isValid
          && (
            <div className="error-message">
              {errorMessage}
            </div>
          )
        }

        <label htmlFor="search">
          <input
            id="search"
            type="text"
          />
        </label>

        <input type="submit" value="Submit" />
      </form>
    )}
  </FormValidationState>
)

export default Form
import createRenderProps from './utils/createRenderProps'
import createReducer from './utils/createReducer'
import createStateSubscriber from './utils/createStateSubscriber'

export const VALIDATE_FIELDS = 'VALIDATE_FIELDS'

export const actions = {
  validateFields: ({
    fieldNames,
    formElement,
  }) => ({
    type: VALIDATE_FIELDS,
    fieldNames,
    formElement,
  }),
}

export const initialState = false

export const reducerActions = {
  [VALIDATE_FIELDS]: (
    prevState,
    {
      fieldNames,
      formElement,
    },
  ) => (
    fieldNames
    .map(fieldName => (
      formElement[fieldName]
      .value
    ))
    .every(Boolean)
  ),
}

export const isValidReducer = (
  createReducer(
    reducerActions,
    initialState,
  )
)

const namespacedReducers = {
  isValid: isValidReducer,
}

const FormValidationState = (
  createStateSubscriber(
    createRenderProps({
      actions,
      namespacedReducers,
    })
  )
)

export default FormValidationState
import createRenderProps from './utils/createRenderProps'
import createReducer from './utils/createReducer'
import createStateSubscriber from './utils/createStateSubscriber'

export const LOADING = 'LOADING'
export const LOADED = 'LOADED'

export const actions = {
  setLoading: () => ({
    type: LOADING,
  }),

  setLoaded: () => ({
    type: LOADED,
  }),
}

export const initialState = false

export const reducerActions = {
  [LOADING]: () => true,
  [LOADED]: () => initialState,
}

export const isLoadingReducer = (
  createReducer(
    reducerActions,
    initialState,
  )
)

const namespacedReducers = {
  isLoading: isLoadingReducer,
}

const LoadingState = (
  createStateSubscriber(
    createRenderProps({
      actions,
      namespacedReducers,
    })
  )
)

export default LoadingState
import React, { Fragment } from 'react'
import ReactDOM from 'react-dom'

import Form from './Form'
import Lifecycles from './Lifecycles'
import LoadingState from './LoadingState'

const App = () => (
  <div>
    <LoadingState>
      {({
        isLoading,
        setLoading,
        setLoaded,
      }) =>
        <Fragment>
          <Lifecycles
            initialize={() => {
              setLoading()

              setTimeout(
                setLoaded,
                2000,
              )
            }}
          />

          {
            isLoading
            ? 'Loading...'
            : 'Loaded'
          }
        </Fragment>
      }
    </LoadingState>

    <Form errorMessage="It's working, but broken???" />
  </div>
)

const rootElement = (
  document
  .getElementById('root')
)

ReactDOM
.render(
  <App />,
  rootElement,
)