Skip to content

Instantly share code, notes, and snippets.

@jamiebuilds
Last active March 6, 2019 21:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamiebuilds/3ab2fa628061e41aa82b3d313cff8b6f to your computer and use it in GitHub Desktop.
Save jamiebuilds/3ab2fa628061e41aa82b3d313cff8b6f to your computer and use it in GitHub Desktop.
Unstated 3.0 proposal
import React from "react"
import { render } from "react-dom"
import { createContextState, useContextState, Provider } from "unstated"
‪let Count = createContextState(0)‬
‪function useCounter() {‬
‪ let [count, setCount] = useContextState(Count)‬
‪ let increment = () => setCount(count + 1)
let decrement = () => setCount(count - 1)
‪ return { count, increment, decrement }‬
‪}‬
function Counter() {
let counter = useCounter()
return <>
Count: {counter.count}
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
</>
}
render(
<Provider>
<Counter/>
</Provider>,
document.getElementById("root")
)
import React from "react"
import { render } from "react-dom"
import { createContextReducer, useContextReducer, Provider } from "unstated"
‪let Count = createContextReducer(0, (state, action) => {
switch (action.type) {
case "increment": return state + 1
case "decrement": return state - 1
default: thrown new Error()
}
})
‪function Counter() {‬
‪ let [count, dispatch] = useContextReducer(Count)‬
return <>
Count: {count}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
}
render(
<Provider>
<Counter/>
</Provider>,
document.getElementById("root")
)
import React from "react"
import { render } from "react-dom"
import { createContextState, useContextState, Provider } from "unstated"
‪let Count = createContextState(0)‬
function Counter() {
let [count] = useContextState(Count)
return <>Count: {count}</>
}
render(
<Provider>
{/* Uses global */}
<Counter/>
{/* Uses isolated with default initialValue */}
<Count.Provider>
<Counter/>
</Count.Provider>
{/* Uses isolated with custom initialValue */}
<Count.Provider initialValue={42}>
<Counter/>
</Count.Provider>
</Provider>,
document.getElementById("root")
)
@benmvp
Copy link

benmvp commented Mar 6, 2019

I really like this new API! Nicely done!

I have mixed feelings about the functions like createContextState or useContextState. Like I get that it's using context under the hood to make it all happen. So you can use people's mental model of React context to help them understand how unstated works.

However, every time I look at the functions in code I keep thinking they are React.createContext & useContext from React, which they aren't. The abstraction doesn't have to use context, it's just what makes the most sense to accomplish global state. Replacing them with something like createGlobalState or useGlobalState would make context an implementation detail. It may also be a bit more semantically closer to what the API is doing.

@jamiebuilds
Copy link
Author

jamiebuilds commented Mar 6, 2019

createGlobalState was actually what I originally had. But the reason I wanted to use context is because it models context directly.

The closet API you could get with the tools React provides you is this:

let Count = React.createContext(null)

function Counter() {
  let [count, setCount] = React.useContext(Count)
  // ...
}

function App() {
  let [count, setCount] = React.useState(0)

  return (
    <Count.Provider value={[count, setCount]}>
      <Counter/>
    </Count.Provider>
  )
}

All I'm trying to do is translate that API to:

let Count = Unstated.createContextState(0)

function Counter() {
  let [count, setCount] = Unstated.useContextState(Count)
  // ...
}

function App() {
  return (
    <Unstated.Provider>
      <Counter/>
    </Unstated.Provider>
  )
}

Which:

  • Eliminates all the setup work at the top-level (App)
  • Eliminates the need to depend directly on Count.Provider
    • Which allows for easier code splitting and only needs to be setup once
  • Optimizes the <Provider value={...}> to not cause re-renders all the time
  • Co-locate the initialValue (0) to where you createContext(State)

But otherwise I want to maintain all the properties of using createContext such as being able to "isolate" state with a new <*.Provider/>:

let Count = Unstated.createContextState(0)

function Counter() {
  let [count, setCount] = Unstated.useContextState(Count)
  // ...
}

function App() {
  return (
    <Unstated.Provider>
      <Counter/>
      <Count.Provider>
        <Counter/>
      </Count.Provider>
    </Unstated.Provider>
  )
}

So I really want people to be thinking of this as context state, and not a global state container.

@benmvp
Copy link

benmvp commented Mar 6, 2019

Ah yeah, I forgot about the nested support. You're right that context makes more sense. It literally is the state from the context in which it resides. Any other synonym would probably cause more confusion than what it'd help.

I'll mull it over some more to see if anything else jumps out at me, but it seems all good to me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment