DEV Community

Cover image for merced-react-hooks => Application State, LocalStorage, Lifecycle
Alex Merced
Alex Merced

Posted on

merced-react-hooks => Application State, LocalStorage, Lifecycle

merced-react-hooks VIDEO PLAYLIST: https://www.youtube.com/playlist?list=PLY6oTPmKnKbYurl9-_fSMY4X6DZ1sx39s

The Purpose

While of course I always enjoy creating new tools, there were a few reasons I wanted to create this library.

  • To reduce boilerplate in very common react patterns (do we really need to create the same handleChange function over and over again)

  • Make the use of existing React features more semantic. (useEffect isn't as semantic as "componentWillMount")

  • Introduce some alternative patterns I think may be good alternatives (wouldn't it be nice to easily do async action in your application state)

merced-react-hooks has several custom hooks that are aimed at accomplishing the above.

Application Level State

Often you have two main choices when achieving application-level state. Using the Redux library which has a lot boilerplate in its setup or use a combo of useContext and useReducer to get a basic redux like setup. I've created two tools

useDataStore

This automates a lot of the boilerplate in the useContext/useReducer path. First, you create a ds.js file with the following.

import { createDataStore } from "merced-react-hooks"

//initialState
const initialState = {
  token: "",
  baseURL: "",
  count,
}

//reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case "add":
      return { ...state, count: state.count + payload }
      break
    case "sub":
      return { ...state, count: state.count - payload }
      break
    default:
      return state
      break
  }
}

//create provider and consumer hook
export const [DataStore, useDataStore] = createDataStore(initialState, reducer)
Enter fullscreen mode Exit fullscreen mode

then you add it to index.js and you are good to go!

import { DataStore } from "./ds.js"

ReactDOM.render(
  <DataStore>
    <App />
  </DataStore>
)
Enter fullscreen mode Exit fullscreen mode

then just use the hook to pull the state and dispatch in the component you plan on using it in.

Component.jsx

import { useDataStore } from "../ds.js"

const Component = props => {
  //pull out the DataStore
  const { dataStore, dispatch } = useDataStore()

  return (
    <>
      <h1>{dataStore.count}</h1>
      <button onClick={() => dispatch({ type: "add", payload: 1 })}>add</button>
      <button onClick={() => dispatch({ type: "sub", payload: 1 })}>add</button>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

TaskRunner

While DataStore certainly simplified the combo of Context/Reducer hooks but the problem with a reducer is the following...

  • The reducer must always return a value that becomes the new value of state
  • You can't do async operations in the reducer function, so async logic would still exist outside your reducer.

So I created the TaskRunner to give a near-identical pattern that addresses the above two problems. The big difference is that instead of a reducer function you are passing taskList object and each key is a function that is called via the taskRun function.

Each function in the taskList is passed the state, setState and payload. So you can decide whether a particular function should even setState at all or do async operations and setState within the async operations.

It all starts with tr.js file in your src folder...

import { createTaskRunner } from "merced-react-hooks"

//initialState
const initialState = {
  token: "",
  baseURL: "",
  count,
}

//reducer function
const taskList = {
  add: (state, setState, payload) =>
    setState({ ...state, count: count + payload }),
  sub: (state, setState, payload) =>
    setState({ ...state, count: count - payload }),
}

//create provider and consumer hook
export const [TaskStore, useTaskStore] = createTaskRunner(
  initialState,
  taskList
)
Enter fullscreen mode Exit fullscreen mode

then you add it to index.js and you are good to go!

import { TaskStore } from "./tr.js"

ReactDOM.render(
  <TaskStore>
    <App />
  </TaskStore>
)
Enter fullscreen mode Exit fullscreen mode

then just use the hook to pull the state and dispatch in the component you plan on using it in.

Component.jsx

import { useTaskStore } from "../tr.js"

const Component = props => {
  //pull out the DataStore
  const { taskStore, runTask } = useTaskStore()

  return (
    <>
      <h1>{dataStore.count}</h1>
      <button onClick={() => runTask("add", 1)}>add</button>
      <button onClick={() => runTask("sub", 1)}>add</button>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

When you compare this to using reducers and dispatch, you have more flexibility and it looks a little less verbose. Win!

useFormState

When doing controlled forms in React there is some annoying boilerplate.

  • creating state for your form
  • creating a handleChange function to update state
  • resetting the form after submission

The useFormState hook does exactly this. You pass it the initialState and it will return the state, a handleChange function, and a resetForm function for resetting the form back to initial.

import { useFormState } from "merced-react-hooks"

const Form = props => {
  const [formData, handleChange, resetForm] = useFormState({
    name: "",
    age: 0,
  })

  const handleSubmit = event => {
    event.preventDefault()
    console.log(formData)
    resetForm()
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      <input
        type="number"
        name="age"
        value={formData.name}
        onChange={handleChange}
      />
      <input type="submit" value="click me" />
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

useLocalStorage and useSessionStorage

Sometimes you need to line up your state with a key in local/session storage. These hooks will...

  • Check if the key already exists and if so set the state
  • If not, create the key and set the state to the initialState argument
  • return the state, setState, and reset function

I highly recommend watching the video on these hooks in the video playlist at the beginning of this post to see this hook in action.

Lifecycle Hooks

The useEffect hook serves all the role of functions like componentWillMount, componentWillUpdate, and ComponentWillUnmount would have. While I prefer the react hooks patterns the semantic names of these functions certainly made the intention of these functions clearer.

I made hooks that are a light abstraction over useEffect to make your code more declarative and semantic.


import {useOnMount, useOnUpdate, useOnDismount} from "merced-react-hooks"

const Child = (props) => {

  useOnDismount(() => console.log("I don't exist anymore"))

  return <h1> Look! I exist </h1>
}

const App = (props) => {

  const [toggle, setToggle] = React.useState(false)

  useOnMount(() => console.log("I happen onMount"))

  useOnUpdate(()=>console.log("I happen on update), [toggle])

  return (<>
    {toggle ? <Child/> : null}
    <button onClick={() => setToggle(!toggle)}>Click Me</button>
  </>)
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

I plan on continuing to add more hooks to this library if your using tree shaking in your build process you'll only be bundling what you need since everything is a named export and no hook depends on another hook in this library. So you got tools and you can use them efficiently! I hope you enjoy!

Top comments (0)