📄
react-states
  • react-states API
  • Introduction
  • Our journey
  • The Mental Model
  • Adopting react-states
  • Reference implementations
  • Environment interface
  • Subscriptions
  • Features
  • Writing UI components
  • Explicit states
  • State transitions
  • Transition effects
  • Feature testing
  • Files and folders
  • Patterns
  • PR review guide
  • Devtools
Powered by GitBook
On this page
  • Matching to styles
  • Matching to variants
  • What is feature and what is UI?
  • Explicit states for complex interactions

Was this helpful?

Writing UI components

PreviousFeaturesNextExplicit states

Last updated 3 years ago

Was this helpful?

Writing UI components in the react-states architecture removes a lot of complexity when writing UIs. All state management, data fetching and environment effects related to the domain of the application is not the concern of any components defining UI. They rather consume this as feature hooks. That does not mean UI components does not have state or side effects, but they are tied to the UI itself, not the domain of the application.

With you get a super power when consuming the feature to create a UI. We can ensure that any state the feature can be in is actually covered by the UI implementation. This is done by matching over the states of the features.

const Auth = () => {
  const [auth, dispatch] = useAuth()
  
  return match(auth, {
    UNAUTHENTICATED: () => <button onClick={
      () => dispatch{ type: 'SIGN_IN' })
    }>Sign In</button>,
    AUTHENTICATING: () => <button disabled={true}>Signing In...</button>,
    AUTHENTICATED: ({ user }) => <Dashboard user={user} />,
    ERROR: ({ error }) => <h4>Oh no, ${error}</h4>
  })
}

If any state is added, changed or removed in the feature you know that the usage of match will notify you about that change in the UI implementation. That means you should favour using match as it gives more predictability.

Matching to styles

The match can map to anything, for example the styling of en element.

const SomeComponent = () => {
  const [auth] = useAuth()
  
  const style = match(auth, {
    UNAUTHENTICATED: () => ({ opacity: 1 }),
    AUTHENTICATING: () => ({ opacity: 0.5 }),
    AUTHENTICATED: () => ({ opacity: 0 }),
    ERROR: () => ({ opacity: 1 })
  })
}

Matching to variants

You can also create your own custom mapping structure which helps defining the UI.

const SomeComponent = () => {
  const [auth] = useAuth()
  
  const variant = match(auth, {
    UNAUTHENTICATED: () => ({
      icon: <FaUnlock />,
      opacity: 1,
      text: 'Sign In'
    }),
    AUTHENTICATING: () => ({
      icon: <FaSpinner />,
      opacity: 0.5,
      text: null'
    }),
    AUTHENTICATED: () => ({}),
    ERROR: () => ({})
  })
}

What is feature and what is UI?

Deciding what logic should be contained within a feature and what is strictly UI related can sometimes be difficult, but you can not really do it wrong, it is only a matter of consequences. The rule of thumb when bringing logic like state and effects into a UI component is to identify if that state and logic is tied to the UI itself, not something related to the application. An example of that would be:

const SomeComponent = () => {
  const [, dispatch] = useSomeFeature()
  const [input, setInput] = useState('')
  
  return (
    <form onSubmit={() => {
      dispatch({
        type: 'SUBMIT_INPUT',
        input
      })
    }}>
      <input value={input} onChange={() => setInput(event.target.value)} />
    </form>
  )
}

The feature does not care about the input coming from an actual HTML input, this is a UI concern.

An other example would be:

const SomeComponent = () => {
  const [, dispatch] = useSomeFeature()
  const inputRef = useRef<HTMLInputElement>(null)
  
  useEffect(() => {
    inputRef.current.focus()
  }, [])
  
  return <input ref={inputRef} /> 
}

The feature does not care about the input getting focus when the component mounts.

But the state of a feature can intersect with UI interaction. For example:

const SomeComponent = () => {
  const [state, dispatch] = useSomeFeature()
  const animation = useAnimation()
  
  useStateEffect(state, 'AUTHENTICATING', () => 
    animation.start()
  )
  
  return <div style={animation.style} /> 
}

Explicit states for complex interactions

The createReducer utility is not restricted to features. You can use it in components to deal with complex UI changes and interactions. An example of that would be drag and drop:

import { createReducer, match, States, StatesTransition } from 'react-states'

type State =
  | {
    state: 'IDLE'
  }
  | {
    state: 'DETECTING_DRAG'
    initialX: number
    initialY: number
  }
  | {
    state: 'DRAGGING'
    x: number
    y: number
  };
  
type Action =
  | {
    type: 'MOUSE_MOVE'
    x: number
    y: number
  }
  | {
    type: 'MOUSE_UP'
    x: number
    y: number
  }
  | {
    type: 'MOUSE_DOWN'
    x: number
    y: number
  }
  
type DragDrop = States<State, Action>

type Transition = StatesTransition<DragDrop>

const reducer = createsReducer<DragDrop>({
  IDLE: {
    MOUSE_DOWN: (_, { x, y }): Transition => ({
      state: 'DETECT_DRAG',
      initialX: x,
      initialY: y
    })
  },
  DETECT_DRAG: {
    MOUSE_MOVE: (detectDragState, { x, y }): Transition => {
      if (
        Math.abs(x - detectDragContext.initialX) > 5 || 
        Math.abs(y - detectDragContext.initialY) > 5
      ) {
        return { state: 'DRAGGING', x, y }
      }
      
      return detectDragContext
    } 
  },
  DRAGGING: {
    MOUSE_MOVE: (_, { x, y }): Transition => ({ state: 'DRAGGING', x, y }),
    MOUSE_UP: (): Transition => ({ state: 'IDLE' }) 
  }
})

export const ComponentA = () => {
  const [dragDrop, dispatch] = useReducer(reducer, {
    state: 'IDLE'
  })
  
  const events = {
    onMouseMove: (event: MouseEvent) => 
      dispatch({
        type: 'MOUSE_MOVE',
        x: event.clientX,
        y: event.clientY
      }),
    onMouseUp: (event: MouseEvent) => 
      dispatch({
        type: 'MOUSE_UP',
        x: event.clientX,
        y: event.clientY
      }),
    onMouseDown: (event: MouseEvent) => 
      dispatch{
        type: 'MOUSE_DOWN',
        x: event.clientX,
        y: event.clientY
      })
  }
  
  const props = match(dragDrop, {
    IDLE: () => events,
    DETECT_DRAG: () => events,
    DRAGGING: ({ x, y }) => ({ ...events, style: { left: x, top: y } })
  })
  
  return <div {...props} />
}
explicit states