Skip to content

Instantly share code, notes, and snippets.

@paulcollett
Last active April 27, 2020 02:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save paulcollett/803327e0bd58590e623c5fbb379489a1 to your computer and use it in GitHub Desktop.
Save paulcollett/803327e0bd58590e623c5fbb379489a1 to your computer and use it in GitHub Desktop.
React Hooks safe component replacement

Alternative interface idea that helps pave over the stale closure gotchas when working with React Hooks.

If you find yourself trying to connect data between multiple hooks like useEffect's and useRef's, in other words writing anything more than simple imperative logic in your components, then this gives you a saftey shield from even the mildest of react hook's dependecy/state state issues

import createComponent from './createComponent'

// Basic Usage
const Example1 = createComponent(() => {
  // Rule 1: return a function that returns jsx
  return () => <div>Hello</div>
});

// Props
// Rule 2: Don't destructure props at any point
const Example2 = createComponent(({ props }) => {
  return () => <div>{props.children}</div>
});

// State
// Rule 3: Don't destructure state at any point
const Example2 = createComponent(({ props, state, setState }) => {
  setState({ counter: 1 }) // aka. initial state

  return () => <div>{state.counter}</div>
});


// Methods
const Example2 = createComponent(({ props, state, setState }) => {
  setState({ counter: 1 })
  
  function inc() {
    setState({ counter: state.counter });
  }
  
  setInterval(() => inc, 1000)

  return () => <div onClick={inc}>{state.counter}</div>
});

// Existing Hooks
const Example2 = createComponent(({ props, state, setState, hooks }) => {
  setState({ counter: 1 })
  let x, y, timeout, myEl

  // Rule 4: Wrap hooks
  hooks(() => {
    myEl = useRef();

    useEffect(() => {
      // did mount (myEl.current is avaliable here)
      return () => clearTimeout(timeout) // did unmount
    }, []);
    
    ({ x, y } = useMousePosition(myEl.current))
  });

  timeout = setInterval(() => setState({ counter: state.counter + 1 }), 1000)

  return () => <div ref={myEl}>{state.counter} {x},{y}</div>
})


// As a Hook
const useMyLogic = createComponent(({ props, state, setState, hooks }) => {
  setState({ count: 0 })
  
  function inc() {
    setState({ count: state.count + 1 })
  }
  
  // Still return a function that returns the data
  // Still don't destructure state
  return () => [ state, inc ]
});
const MyControllerComponent = createComponent(({ hooks }) => {
  let counterstate, inc
  hooks({
    [ counterstate, inc ] = useMyLogic({ /* props */ })
  })
  return () => <div onClick={inc}>{counterstate.count}</div>
})
const MyNormalComponent = (() => {
  const [ counterstate, inc ] = useMyLogic({ /* props */ })

  return <div onClick={inc}>{counterstate.count}</div>
})
import React, { useEffect, useRef, useReducer } from 'react'
import { useState } from 'react'
const createComponent = (setup) => {
return (rawProps) => {
const hookCallbacks = useRef(new Set([]))
const instance = useRef()
const props = useRef({})
const state = useRef({})
const [, runRender] = useReducer((counter) => counter + 1, 0)
props.current = Object.assign(props.current, rawProps);
if(!instance.current) {
instance.current = setup({
setState(newState) {
Object.assign(state.current, newState)
// only run it on subsequent
if(instance.current) {
runRender()
}
return state.current
},
props: props.current,
state: state.current,
hooks: (cb) => hookCallbacks.current.add(cb),
})
}
hookCallbacks.current.forEach(cb => cb())
if(typeof instance.current !== 'function') {
throw new Error("return must be a function")
}
return instance.current();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment