Git Product home page Git Product logo

unstated-next's Introduction

English | 中文 | Русский | ภาษาไทย | Tiếng Việt
(Please contribute translations!)

Unstated Next

200 bytes to never think about React state management libraries ever again

  • React Hooks use them for all your state management.
  • ~200 bytes min+gz.
  • Familiar API just use React as intended.
  • Minimal API it takes 5 minutes to learn.
  • Written in TypeScript and will make it easier for you to type your React code.

But, the most important question: Is this better than Redux? Well...

  • It's smaller. It's 40x smaller.
  • It's faster. Componentize the problem of performance.
  • It's easier to learn. You already will have to know React Hooks & Context, just use them, they rock.
  • It's easier to integrate. Integrate one component at a time, and easily integrate with every React library.
  • It's easier to test. Testing reducers is a waste of your time, make it easier to test your React components.
  • It's easier to typecheck. Designed to make most of your types inferable.
  • It's minimal. It's just React.

So you decide.

Install

npm install --save unstated-next

Example

import React, { useState } from "react"
import { createContainer } from "unstated-next"
import { render } from "react-dom"

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <Counter.Provider initialState={2}>
        <div>
          <div>
            <CounterDisplay />
          </div>
        </div>
      </Counter.Provider>
    </Counter.Provider>
  )
}

render(<App />, document.getElementById("root"))

API

createContainer(useHook)

import { createContainer } from "unstated-next"

function useCustomHook() {
  let [value, setValue] = useState()
  let onChange = e => setValue(e.currentTarget.value)
  return { value, onChange }
}

let Container = createContainer(useCustomHook)
// Container === { Provider, useContainer }

<Container.Provider>

function ParentComponent() {
  return (
    <Container.Provider>
      <ChildComponent />
    </Container.Provider>
  )
}

<Container.Provider initialState>

function useCustomHook(initialState = "") {
  let [value, setValue] = useState(initialState)
  // ...
}

function ParentComponent() {
  return (
    <Container.Provider initialState={"value"}>
      <ChildComponent />
    </Container.Provider>
  )
}

Container.useContainer()

function ChildComponent() {
  let input = Container.useContainer()
  return <input value={input.value} onChange={input.onChange} />
}

useContainer(Container)

import { useContainer } from "unstated-next"

function ChildComponent() {
  let input = useContainer(Container)
  return <input value={input.value} onChange={input.onChange} />
}

Guide

If you've never used React Hooks before, I recommend pausing and going to read through the excellent docs on the React site.

So with hooks you might create a component like this:

function CounterDisplay() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>You clicked {count} times</p>
      <button onClick={increment}>+</button>
    </div>
  )
}

Then if you want to share the logic behind the component, you could pull it out into a custom hook:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

function CounterDisplay() {
  let counter = useCounter()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

But what if you want to share the state in addition to the logic, what do you do?

This is where context comes into play:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContext(null)

function CounterDisplay() {
  let counter = useContext(Counter)
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  let counter = useCounter()
  return (
    <Counter.Provider value={counter}>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}

This is great, it's perfect, more people should write code like this.

But sometimes we all need a little bit more structure and intentional API design in order to get it consistently right.

By introducing the createContainer() function, you can think about your custom hooks as "containers" and have an API that's clear and prevents you from using it wrong.

import { createContainer } from "unstated-next"

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

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

Here's the diff of that change:

- import { createContext, useContext } from "react"
+ import { createContainer } from "unstated-next"

  function useCounter() {
    ...
  }

- let Counter = createContext(null)
+ let Counter = createContainer(useCounter)

  function CounterDisplay() {
-   let counter = useContext(Counter)
+   let counter = Counter.useContainer()
    return (
      <div>
        ...
      </div>
    )
  }

  function App() {
-   let counter = useCounter()
    return (
-     <Counter.Provider value={counter}>
+     <Counter.Provider>
        <CounterDisplay />
        <CounterDisplay />
      </Counter.Provider>
    )
  }

If you're using TypeScript (which I encourage you to learn more about if you are not), this also has the benefit of making TypeScript's built-in inference work better. As long as your custom hook is typed, then everything else will just work.

Tips

Tip #1: Composing Containers

Because we're just working with custom React hooks, we can compose containers inside of other hooks.

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment, setCount }
}

let Counter = createContainer(useCounter)

function useResettableCounter() {
  let counter = Counter.useContainer()
  let reset = () => counter.setCount(0)
  return { ...counter, reset }
}

Tip #2: Keeping Containers Small

This can be useful for keeping your containers small and focused. Which can be important if you want to code split the logic in your containers: Just move them to their own hooks and keep just the state in containers.

function useCount() {
  return useState(0)
}

let Count = createContainer(useCount)

function useCounter() {
  let [count, setCount] = Count.useContainer()
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  let reset = () => setCount(0)
  return { count, decrement, increment, reset }
}

Tip #3: Optimizing components

There's no "optimizing" unstated-next to be done, all of the optimizations you might do would be standard React optimizations.

1) Optimizing expensive sub-trees by splitting the component apart

Before:

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
      <div>
        <div>
          <div>
            <div>SUPER EXPENSIVE RENDERING STUFF</div>
          </div>
        </div>
      </div>
    </div>
  )
}

After:

function ExpensiveComponent() {
  return (
    <div>
      <div>
        <div>
          <div>SUPER EXPENSIVE RENDERING STUFF</div>
        </div>
      </div>
    </div>
  )
}

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
      <ExpensiveComponent />
    </div>
  )
}

2) Optimizing expensive operations with useMemo()

Before:

function CounterDisplay(props) {
  let counter = Counter.useContainer()

  // Recalculating this every time `counter` changes is expensive
  let expensiveValue = expensiveComputation(props.input)

  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

After:

function CounterDisplay(props) {
  let counter = Counter.useContainer()

  // Only recalculate this value when its inputs have changed
  let expensiveValue = useMemo(() => {
    return expensiveComputation(props.input)
  }, [props.input])

  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

3) Reducing re-renders using React.memo() and useCallback()

Before:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay(props) {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

After:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = useCallback(() => setCount(count - 1), [count])
  let increment = useCallback(() => setCount(count + 1), [count])
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

let CounterDisplayInner = React.memo(props => {
  return (
    <div>
      <button onClick={props.decrement}>-</button>
      <p>You clicked {props.count} times</p>
      <button onClick={props.increment}>+</button>
    </div>
  )
})

function CounterDisplay(props) {
  let counter = Counter.useContainer()
  return <CounterDisplayInner {...counter} />
}

4) Wrapping your elements with useMemo()

via Dan Abramov

Before:

function CounterDisplay(props) {
  let counter = Counter.useContainer()
  let count = counter.count
  
  return (
    <p>You clicked {count} times</p>
  )
}

After:

function CounterDisplay(props) {
  let counter = Counter.useContainer()
  let count = counter.count
  
  return useMemo(() => (
    <p>You clicked {count} times</p>
  ), [count])
}

Relation to Unstated

I consider this library the spiritual successor to Unstated. I created Unstated because I believed that React was really great at state management already and the only missing piece was sharing state and logic easily. So I created Unstated to be the "minimal" solution to sharing React state and logic.

However, with Hooks, React has become much better at sharing state and logic. To the point that I think Unstated has become an unnecessary abstraction.

HOWEVER, I think many developers have struggled seeing how to share state and logic with React Hooks for "application state". That may just be an issue of documentation and community momentum, but I think that an API could help bridge that mental gap.

That API is what Unstated Next is. Instead of being the "Minimal API for sharing state and logic in React", it is now the "Minimal API for understanding shared state and logic in React".

I've always been on the side of React. I want React to win. I would like to see the community abandon state management libraries like Redux, and find better ways of making use of React's built-in toolchain.

If instead of using Unstated, you just want to use React itself, I would highly encourage that. Write blog posts about it! Give talks about it! Spread your knowledge in the community.

Migration from unstated

I've intentionally published this as a separate package name because it is a complete reset on the API. This way you can have both installed and migrate incrementally.

Please provide me with feedback on that migration process, because over the next few months I hope to take that feedback and do two things:

  • Make sure unstated-next fulfills all the needs of unstated users.
  • Make sure unstated has a clean migration process towards unstated-next.

I may choose to add APIs to either library to make life easier for developers. For unstated-next I promise that the added APIs will be as minimal as possible and I'll try to keep the library small.

In the future, I will likely merge unstated-next back into unstated as a new major version. unstated-next will still exist so that you can have both unstated@2 and unstated-next installed. Then when you are done with the migration, you can update to unstated@3 and remove unstated-next (being sure to update all your imports as you do... should be just a find-and-replace).

Even though this is a major new API change, I hope that I can make this migration as easy as possible on you. I'm optimizing for you to get to using the latest React Hooks APIs and not for preserving code written with Unstated.Container's. Feel free to provide feedback on how that could be done better.

unstated-next's People

Contributors

bvanrijn avatar fnky avatar go7hic avatar jamiebuilds avatar jprogrammer avatar naruepanart avatar nateradebaugh avatar nusr avatar paul-phan avatar yallie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

unstated-next's Issues

Translate to Portuguese

Hello,

How can I send a translation to Portuguese?
Can I just send a PR with the document translated?

Moving from unstated to unstated-next

I had hoped to do a gradual move from unstated to unstated-next, but it's much trickier than I had expected, since I can't access the unstated-next state from any component that is still a class.
Are there any good strategies or techniques for an incremental move?

Failing this, would you consider one more release of unstated, with the react calls renamed to the UNSAFE_ versions? That would at least buy me some time to make changes (until React 17) without exposing the long ugly chrome console message from React.

How to use with webhooks etc?

I love the idea of this library and using built in React to manage state. One area where I think this will be an issue is things like webhooks, where I have external events driving state changes. In redux, it's easy. Just dispatch an action based on the message. With something like this though, the logic to handle connecting to a webhook would need to be inside a component, and all state changes would need to be explicitly calling out to reducers in the component etc.

Not sure if theres a better way to go about that, really interested in simplifying my state management without losing explicitness. Thanks!

Status, relationship to unstated?

@basarat had nice things to say, so I'm here.

Can you say something about the likely stability of this project, and best way to adopt it now? I know it's only been public 5 days, and there are no releases or even tags. For instance, is it best to fork for a while? You might want to add this kind of guidance to the readme.

Similar, but not as good imvho: outstated

TypeError: useHook is not a function with Next.js

I'm using unstated-next with Next.js.
Do you know this error?

Unhandled Runtime Error
TypeError: useHook is not a function

Call Stack
Provider
node_modules/unstated-next/dist/unstated-next.mjs (7:0)
renderWithHooks
mountIndeterminateComponent
beginWork
HTMLUnknownElement.callCallback
Object.invokeGuardedCallbackDev
invokeGuardedCallback
beginWork$1
performUnitOfWork
workLoopSync
performSyncWorkOnRoot
scheduleUpdateOnFiber
updateContainer
<unknown>
unbatchedUpdates
legacyRenderSubtreeIntoContainer
Object.hydrate
renderReactElement
_callee3$
tryCatch
Generator.invoke [as _invoke]
Generator.prototype.<computed> [as next]
asyncGeneratorStep
_next
eval
new Promise
<anonymous>
eval
_doRender
doRender
_callee2$
tryCatch
Generator.invoke [as _invoke]
Generator.prototype.<computed> [as next]
asyncGeneratorStep
_next
eval
new Promise
<anonymous>
eval
_render
render
eval
eval

Support Storybook

When I use .useContainer() in a component, Storybook complains that I have not given it a <Container.Provider>.

I have tried wrapping the Story with a Provider, but have had no luck. Have you ran into this issue before?

Can selectors be supported?

e.g.

const count = Counter.useSelector(state => state.count)

Doing this only triggers rerender when count changes.

It would be perfect if you supported this.

Why performance better than Redux?

But, the most important question: Is this better than Redux? Well...

It's faster. Componentize the problem of performance.

Could you please describe why or under what conditions Unstated could offer better performance than Redux?

Hot Reload / Holding State?

Currently using unstated-next and I am in love. One small quirk I found moving from Redux is that the state held in the Context API doesn't seem to hold itself past reloads, unlike how Redux functions. I assume this is because of the way hooks work.

Does anyone have any patterns or suggestions they can recommend as to improve my DX flow? Much appreciated.

[Question] How does splitting the component apart optimize performance

In README there's a performance optimization method

  1. Optimizing expensive sub-trees by splitting the component apart

I don't quite understand how it optimize performance. IMO, the separated component render as much times as before separating and I made a small demo: https://codesandbox.io/s/dark-http-selxh. The ExpensiveComponent will console as long as parent component's useContainer value changed.

Could anybody tell me about the correct understanding of it? 🤔

Debug ability

Hello, mister @jamiebuilds

First of all, thank you very much for your incredible coding.
Is there any work going on for debugging containers with this hooks paradigm?

Thank you again!

Can I use unstated-next to store non-serializable data?

Hi 👋

Redux strongly recommends to only keep serializable data in state, they have efficiencies around applying diffs, time travel, persistence, etc.

I want to store object instances like WebRTC streams and use a state management for that, I am wondering if unstated-next would behave well in this case?

Thanks for sharing this awesome project with the world! 💜

useCallback usage

Should the decrement and increment functions be wrapped in useCallback?

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

Integrate with redux?

I'm wondering if it might make sense to try to "support" (exact meaning TBD) mounting containers at specified points in a redux store. Motivations might be:

  • support of redux dev-tools and similar redux tooling
  • easier test setup for some tests
  • hybrid usage of redux and unstated-next for projects in transition

This seems like it would be easy, but I could easily be missing the elephant in the room.

This might be relevant to #10 .

Anyway to use Apollo Queries inside containers?

Anyway to integrate https://github.com/trojanowski/react-apollo-hooks into our containers?

Is something like this is possible? Which fires on an onChange hook?

const addExistingCreds = (() => {
  const { data } = useQuery(CREDENTIALS_BY_EDORG, {
    variables: {name: childEdOrg},
  });
  const existingCreds = data.allEducationalOrganizations[0].credentials;
  return setCredential((prevCreds) => {
    return update(prevCreds, { $set: existingCreds });
  });
});

Selectors?

How would one go about adding selectors to a container?

For example, if a component wanted to use only the count variable from this container, it shouldn't re-render when anotherCount is changed.

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState);
  let [anotherCount, setAnotherCount] = useState(initialState);

  let decrement = () => setCount(count - 1);
  let increment = () => setCount(count + 1);

  let decrementAnother = () => setAnotherCount(anotherCount - 1);
  let incrementAnother = () => setAnotherCount(anotherCount + 1);

  return { count, decrement, increment, anotherCount, decrementAnother, incrementAnother };
}

let Counter = createContainer(useCounter);

Persistence

Is there any library or middleware to persist the state?

strange error

I have a strange error when trying to export a container:

Exported variable 'ThemeProvider' has or is using name 'ContainerProviderProps' from external module

import { useState } from 'react';
import { createContainer } from 'unstated-next';

function useTheme() {
  const [settings, setTheme] = useState<string>('default');

  return { settings, setTheme };
}

const { Provider: ThemeProvider, useContainer } = createContainer(useTheme);

export { ThemeProvider, useContainer };

maybe it is not due to this lib

Unstated vs Unstated-Next

Unstated-Next use React Hooks directly while Unstated create a Container class act like React Component but they are not equal. So you can call function of Container using after "new" outside React and then do the dependency injection back. But this cannot be done in Unstated-Next.

https://github.com/zeit/next.js/blob/master/examples/with-unstated/pages/_app.js
With Unstated, you can see the api could be persist (resetStore, initStore, as well as custom actions if exists) among server-side and client-side. But this also cannot be done with Unstated-Next.

If Unstated-Next aims to be a replacement of Unstated, this should be an issue.

If Unstated-Next is a state management library using React-Hook and have a rule that the state must live in React, then Unstated-Next does not aims to replace Unstated, the README should be updated to clarify the differences and this issue could be closed.

I think both libraries should be exist. Just like Context API + Hooks Combo cannot completely replace Redux.

[Question] Understand how to solve the circular dependency issue with Contexts

I have two Contexts.

  1. User context
  2. Authentication context
    I have both of them dependent on each other. Eg: signout is in auth. But create user is dependent on authentication as it has to set user as authenticated after getting the user. And authentication context would have to set user on the user context after login. But the application throws an error as the order in which the application is wrapped can be either be in one way like
<AuthContainer.Provider>
      <UserContainer.Provider>

or

<UserContainer.Provider>
  AuthContainer.Provider>

And this would throw an error like this
image

I would like to know if there is any way to solve this.

Gradual migration path from unstated

The readme contains some promises about making sure that unstated-next fulfills all the needs of unstated users and that there is a clean migration path.

In reality we found that we don't have a clean path that doesn't involve a massive rewrite.

To facilitate a more gradual migration, I created unstated-retro.

@jamiebuilds looking for your feedback here. Read the FAQ at the bottom about a gradual "child-first" migration path.

Maximum update depth exceeded when used with router for authentication

import { useState } from "react"
import { createContainer } from "unstated-next"
function useAuth() {
    let [auth, setAuth] = useState(0)
    let setAuthDetails = (auth) => setAuth(auth)
    return { auth, setAuthDetails }
}
let AuthContainer = createContainer(useAuth)
export default AuthContainer
import React from "react";
import { Route, BrowserRouter as Router } from "react-router-dom";
import PrivateRoute from "./PrivateRoute"
import Login from "../pages/login/login"
import Home from "../pages/home/home"
import AuthContainer from '../containers/auth.container'

class ReactRouter extends React.Component {
    render() {
        return (
            <AuthContainer.Provider>
                <Router>
                    <Route exact path="/" component={Home} />
                    <PrivateRoute path="/login" component={Login} />
                </Router>
            </AuthContainer.Provider>
        )
    }
}

export default ReactRouter;
import { Route, Redirect } from "react-router-dom";
import React from "react";
import { useContainer } from "unstated-next"

import AuthContainer from "../containers/auth.container";

function PrivateRoute({ component: Component, ...rest }) {
    let authed = useContainer(AuthContainer)
    // let authed = true
    console.log(authed.auth)
    return (
        <Route
            {...rest}
            render={(props) => authed === true
                ? <Component {...props} />
                : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
        />
    )
}

export default PrivateRoute

Multiple calls in same rendering cycle references old state

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

then

counter.increment()
counter.increment()

=> counter.count === 1

For example, this could be caused by immediate error callback of API client.

function useStore(initialState = 0) {
  const [state, setState] = useState(initialState)
  const onInput = (input) => setState({ ...state, input })
  const onRequestStart = () => setState({ ...state, loading: true, sentInput: state.input })
  const onRequestFailed = () => setState({ ...state, loading: false })
  return { state, decrement, increment }
}

const [state, actions] = Store.useContainer()
actions.onRequestStart()
api.call(params, (data, error) => {
  if (error) actions.onRequestFailed() // could reference old state value, "sentInput" value will be lost.
})

Maybe related: #26

Use with Class components

I understand the hooks are not directly supported in Class components, however, in some cases, the global states from unstated-next are needed in Class components.
Is there an elegant way of accessing the state instead of wrapping a Class component in another function component?

[Question] What are some recommended patterns of clearing multiple containers at once?

If I have multiple containers and I need to reset the state to initialState of each one upon let say when a user logs out. What is the best way of doing this? Not having each container's Provider on the App component makes this a bit challenging... Loving the library so far - just running into these little quirks. Open to suggestions and maybe other ways of tackling this problem. Thanks!

Should the initialState prop to Provider be mandatory if initialState is used?

Hi! 👋

I made a container that accepts a (mandatory) initial state:

type InitialState = {
  person: string;
}

function useFilters({ person }: InitialState) {
 // code that assumes `person` is defined
}

const { Provider, useContainer } = createContainer(useFilters);	

export { Provider as EventFiltersProvider, useContainer as useEventFilters };

I was very pleased to see that the initialState prop to the Provider is properly typed:

<EventFiltersProvider
  initialState={{ person:  }}
>
  // …
</EventFiltersProvider>

However, the Provider component does not require the initialState prop, so this also passes the TypeScript compilation:

<EventFiltersProvider>
  // …
</EventFiltersProvider>

I understand that it may be desirable to make a component accepts an optional default state, but it still feels bad that it's possible to make my TypeScript code fail at runtime by forgetting to pass a prop.

I'm not entirely sure what the correct solution to this problem is, but I've at least tried to describe the issue here. 😄

Composing Containers throws Error: Component must be wrapped with <Container.Provider>

Trying to compose containers like so... from the example

const CredentialContainer = createContainer(useCredentials);
const SpecializationContainer = createContainer(useSpecializations);
const EdOrgContainer = createContainer(useEdOrgs);

function useForm() {
    const credentialContainer = CredentialContainer.useContainer();
    const specializationContainer = SpecializationContainer.useContainer();
    const edOrgContainer = EdOrgContainer.useContainer();

    return {...credentialContainer, ...specializationContainer, ...edOrgContainer};
}


const CredentialAndSpecialization = createContainer(useForm)

In Display...

function CredentialAndSpecializationDisplay() {
  const form = CredentialAndSpecialization.useContainer();
  ...

Page ....

  <CredentialAndSpecialization.Provider>
    <CredentialAndSpecializationDisplay />
  </CredentialAndSpecialization.Provider>

Thanks!!!

Error: Component must be wrapped with <Container.Provider>

I'm using react-hot-loader, and whenever I update a container, I get the above error. It seems to happen when a sub-component uses the container that's provided by a parent component. I can't figure out how to fix this.

It doesn't seem to matter whether I use MyContainer.useContainer() or useContainer(MyContainer) - still the same result.

How to avoid Provider hell?

This is a very simple yet versatile state management solution with support for hooks - props for that.

I'd like to know whether there is a way to avoid the Provider hell:

<Store1.Provider>
  <Store2.Provider>
     <Store3.Provider>
       <App />
     </Store3.Provider>
  </Store2.Provider>
</Store1.Provider>

when all I want to do is:

<Provider stores=[Store1, Store2, Store3]>
  <App />
</Provider>

Debug ability

Hello, mister @jamiebuilds

First of all, thank you very much for your incredible coding.
Is there any work going on for debugging containers with this hooks paradigm?

Thank you again!

Reusable functions that leverage Containers outside components

Let's imagine I have components with similar logic, example:

import React from 'react'
import { AppContainer } from '../container/app'
import "..."

const FileListComponent: FC = () => {
  const app = AppContainer.useContainer()

  const fetchFileList = async () => {
    let myFileList: SavedFileList
    try {
      myFileList = await fetchList()
    } catch (error) {
      return errorToast(error)
    }
    app.replaceFileList(myFileList)
  }

  return (
    <div>
    {app.fileList.map(file => (<p>{file.name}</p>))}
    </div>
  )
}

I would like to bring the function myFileList out of that, but it has typing problems:

const fetchFileList = async (app: AppContainer) => {
  let myFileList: SavedFileList
  try {
    myFileList = await fetchList()
  } catch (error) {
    return errorToast(error)
  }
  app.replaceFileList(myFileList)
}

As AppContainer is a value, not a type, and it cannot be used that way. Is there a suitable typing solution for this?

multiple containers - Provider Inject

What is the recommanded way to have multiple containers provided to our App?
in unstated you could use dependency injection:

<Provider inject={[container1, container2...]}>
 MyApp...
<Provider>

but it seems that with next the only way in a tree hell:

<Container1.Provider>
   <Container2.Provider>
     MyApp
   </Container2.Provider>
</Container1.Provider>

Am i missing something?

wait until state changed ?

in unstated, we can use await to wait for state change, like this

class CounterContainer extends Container {
  state = { count: 0 };
  increment = async () => {
    await this.setState({ count: 1 });
    console.log(this.state.count); // 1
  };
}

how do I do something similar in unstated-next ?

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={() => {
        counter.increment();
        console.log(counter.count); // old value
      }}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

Double references necessary?

Hi,

Looking for a new state management lib for my next project, and like the idea of Unstated. I can't seem to understand the necessity to use Provider and the CounterDisplay twice for each? Is this a bug, or how the API works?

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <Counter.Provider initialState={2}>
        <div>
          <div>
            <CounterDisplay />
          </div>
        </div>
      </Counter.Provider>
    </Counter.Provider>
  )
}

Apps with many containers

I'm trying to get my head around the new approach and there are a few things I'm getting stuck on. Part 1 - if I have 10 containers (as I do in my unstated implementation), do I now have to list every one of them at the top level? I certainly appreciated just being able to wrap my App component in <Provider><App /></Provider> and be done with it.

how to use await in unstated-next

hi

i am a noob

in unstated, i can used async like this:

class CounterContainer extends Container {
  state = { count: 0 };
  increment = async () => {
    await this.setState({ count: 1 });
    console.log(this.state.count); // 1
  };
}
 

in unstated-next, how to do this, can you make a demo ?

thank you very much

Testing in unstated-next?

I read through issue #20 and that had some good perspective. Agree on keeping App away. Some tricks there that I could have used a year ago. 🎈 Anyway ...

I could test unstated "actions" through enzyme but I'm looking at this store.js file I made and it's so simple. I just want to test it. Of course, I can't because it needs hooks, context. It needs to be in a DOM/Tree/JSX place just like the used source file.

Something like this:

// store.test.js
import { StoreContainer } from '../src/store'

describe("the state store", () => {

  // the state here is just flipping a checkbox, a spike
  it("can receive an event", () => {
    // this can't work outside a provider
    const store = StoreContainer.useContainer()

    const event = { target: { value: "test" } }
    const checked = false

    store.handleUpdate(event, checked)

    const expected = { "test": false }
    expect(store.filters).toBe(expected)
  })

})

Running this with jest prints Invariant Violation: Invalid hook call. Yeah. That makes sense. "Hooks can only be called inside of the body of a function component." I see in the repo where the context is expected.

My store.js file looks like this. Works fine great when manually testing (like in the app, in the browser, integrated). But I'm adding jest and want to add complexity. So I thought I'd just test my store in isolation and then later the increasing number of "actions". Maybe this is a "bad" idea or what you wanted to avoid in the first place.

// store.js
import { useState } from 'react'
import { createContainer } from "unstated-next"

const useStore = () => {
  const defaultFilters = {}

  const [filters, updateFilters] = useState(defaultFilters)
  const handleUpdate = (event, checked) => {
    let newState = { [event.target.value]: checked }
    updateFilters({ ...filters, ...newState })
  }

  return { filters, handleUpdate }
}

export const StoreContainer = createContainer(useStore)

Filters is just an object that I'm merging with checkbox clicks (a spike).
See, it's just a little bit of code. Maybe I'm wanted to test it for the wrong reasons. Maybe I'm trying to test it like it's a plain js file when it's not.

In the README:

Testing reducers is a waste of your time, make it easier to test your React components.

Do you mean that I should not test the store directly? It's too simple? I could test the store through my components? (normal enzyme stuff)

react-native-navigation compatibility

...and the second thing I'm confused about (with both unstated and unstated-next) is how it integrates with something like react-native-navigation. The trick there is there is seemingly no "top level component" - they are switching it out somewhat frequently. More detail in my issue on RNN

I don't want to lose the context/container state on those transitions, but it's unclear how to avoid that without a "store like" concept ala Redux. It feels like perhaps a weakness of RNN, but maybe I'm missing something about how contexts work and how to get the desired behavior?

Can't use Store methods outside of function?

With these new changes does that mean we can't do things like import the store on a page and initialize it in a constructor? With NextJS, I was using store methods in getInitialProps which is a pre-render action.

Notice in example how there's an Store.init(serverState) called to initialize the store in the constructor.

import App, { Container } from 'next/app'
import React from 'react'
import { Provider } from 'unstated'
import { counterStore } from '../containers/CounterContainer'

class MyApp extends App {
  static async getInitialProps () {
    // do your server state here
    if (!process.browser) {
      // reset state for each request
      counterStore.resetState()
      // process state, in this case counter start with 999
      counterStore.initState(999)
      return { serverState: counterStore.state }
    } else {
      return {}
    }
  }
  constructor (props) {
    super(props)
    // pass the state to client store
    // serverState will be reset when client navigate with Link
    if (process.browser) {
      counterStore.initState(props.serverState.count)
    }
  }
  render () {
    const { Component, pageProps } = this.props
    return (
      <Container>
        <Provider inject={[counterStore]}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    )
  }
}

export default MyApp

Can we pass just functions or states to a component

Is it possibble to pass only functions to a component or can i pass just the states? Like redux mapDispatchToProps and mapDispatchToState functions. For example, if have a component containing just buttons to change state, and i don't want to rerender the component containing buttons, what should i do then? Thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.