jeudi 25 juin 2020

React Hooks: Pitfalls with this Global State pattern?

I'm starting to dive into hooks and to get a better understanding of them I am attempting to supply global state to my application where appropriate. I followed a few tutorials but nothing was producing the desired outcome I wanted from an aesthetic and functional point of view. The issues mostly stemmed around my data being from an API endpoint, where all the tutorials were demonstrating with static data. I landed on the following, and although I've confirmed it works I'm concerned there may be pitfalls that I'm not anticipating.

Before trying to shim this into a real world application where there may be repercussions that I don't find until much later in the build process I was hoping you fine folks could look it over and let me know if there's anything that stands out as being an antipattern. Specifically, I'm looking to see if there are side effects this pattern would produce that would be undesirable, such as extensive loading, infinite loops, data dead zones, ect. once scaled.

Within my /store.ts file I'm creating the general store context:

import React from 'react';

export const catFactsStore = React.createContext<{ state: {}, dispatch: any }>({ state: {}, dispatch: {} })

Within /providers/CatFactsProvider I'm passing the state as well as a dispatcher for a useReducer hook. The useReducer will allow me to set the current value of the state based on its current lifecycle:

import React, { useReducer } from 'react'
import { catFactsStore } from '../store'

const CatFactsProvider: React.FC = ({ children }): JSX.Element => {
  const [state, dispatch] = useReducer(
    (state: any, action: any) => {
      switch (action.type) {
        case 'loading':
          return { ...state, isLoading: true, hasErrored: false, entities: {} }
        case 'success':
          return { ...state, isLoading: false, hasErrored: false, entities: action.payload }
        case 'error':
          return { ...state, isLoading: false, hasErrored: true, errorMessage: action.payload }
        default:
          throw new Error()
      }
    },
    { entities: {}, isLoading: false, hasErrored: false, errorMessage: '' },
  )

  return <catFactsStore.Provider value=>{children}</catFactsStore.Provider>
}

export { catFactsStore, CatFactsProvider }

In index.tsx I'm wrapping my App component with the Provider to make it globally available.

import React from 'react'
import ReactDOM from 'react-dom'
import App from 'src/components/App'
import { CatFactsProvider } from './providers/CatFactProvider'

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

Then I'm creating a higher order component in /components/hoc/withCatFacts to make the actual API request to retrieve the data. The HOC is responsible for dispatching to my reducer and supplying the state to its child components:

import React, { useLayoutEffect, useContext } from 'react'
import { catFactsStore } from '../../store'
import axios from 'axios'

export interface WithCatFactsProps {
  catFactsState: { entities: {}; isLoading: boolean; hasErrored: boolean; errorMessage: string }
}

export default <P extends WithCatFactsProps>(
  ChildComponent: React.ComponentType<P>,
): React.ComponentType<Omit<P, keyof WithCatFactsProps>> => {
  const CatFactsFetcherHOC: React.FC<WithCatFactsProps> = (props): JSX.Element => {
    const { state, dispatch } = useContext(catFactsStore)

    // useLayoutEffect to ensure loading is dispatched prior to the initial render
    useLayoutEffect(() => {
      dispatch({ type: 'loading' })
      axios
        .get('https://cat-fact.herokuapp.com/facts')
        .then((response) => {
          dispatch({ type: 'success', payload: response.data.all })
        })
        .catch((e) => {
          dispatch({ type: 'error', payload: e.message })
        })
    }, [dispatch])

    return <ChildComponent {...(props as P)} catFactsState={state} />
  }

  return CatFactsFetcherHOC as any
}

Finally, within the /components/App.tsx file I'm wrapping my export with withCatFacts and utilizing the data to display in my app:

import React from 'react'
import withCatFacts, { WithCatFactsProps } from './hoc/withCatFacts'

interface Props {}
type CombinedProps = Props & WithCatFactsProps

const App: React.FC<CombinedProps> = ({ catFactsState }): JSX.Element => {
  if (catFactsState.isLoading) {
    return <div>LOADING</div>
  }
  if (catFactsState.hasErrored) {
    return <div>{catFactsState.errorMessage}</div>
  }
  console.log(catFactsState) // Can successfully see Cat Facts, Huzzah!
  return <div>Cat Facts!</div>
}

export default withCatFacts(App)

Any suggestions or comments would be appreciated. With my hook knowledge being limited I don't know what I don't know and would like to catch any problems before they occur.

Aucun commentaire:

Enregistrer un commentaire