Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unstated vs Unstated-Next #20

Closed
raymondsze opened this issue May 15, 2019 · 2 comments
Closed

Unstated vs Unstated-Next #20

raymondsze opened this issue May 15, 2019 · 2 comments

Comments

@raymondsze
Copy link

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.

@jamiebuilds
Copy link
Owner

Have you ever wondered: "Why do I even have an <App/> component?"

Seriously, what purpose does an <App/> component serve other than making your components implicitly dependent on a global environment?

Imagine a world where your index.js file looks like this:

import React from "react"
import { render } from "react-dom"
import { Routes } from "./components/Routes"

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

Nothing to setup, nothing to initialize, no global application wrapper. Then inside your <Routes/> component you just have:

import React from "react"
import { Router } from "@reach/router"

let Home = React.lazy(() => import("./Home"))
let About = React.lazy(() => import("./About"))

export function Routes() {
  return (
    <Router>
      <Home path="/"/>
      <About path="/about"/>
    </Router>
  )
}

Still no global wrapper, still no initialized state, just a mapping of routes to components.

But why would we want to do that?

Well, for one there's no implicit dependencies on some global app state, or the way that the app is initialized. Without even seeing the <Home/> or <About/> components, we know that they can be rendered anywhere... in any React app... anywhere in the component tree.

These components create, subscribe, and persist any state that they depend on. They make sure to fetch any resources they need, they setup their entire environment themselves, they found ways of caching and persisting data that doesn't depend on the application being setup in a particular way.

These components are super easy to test, all you have to do is render them with props and make assertions about them. Your unit tests are much more effective, in fact they feel like integration tests, because the unit you're working with is so self-contained. You can feel much more confident in your tests because external factors won't so easily break these components.

When you work on components like this, you can think about just that component. You don't have to worry about the application that is wrapped around it. You're not climbing back up the tree worrying about other components, or reducers, or whatever. You can focus.

Even further, what if every child component was also built like this? Self-contained systems all the way down. So no matter where you were in your codebase, you could think about just one component at a time. Every component in your app can be moved around to any other part of your app and you don't even have to think about it.

Oh and did I mention that this makes code splitting so easy that you can do it to any component in your entire app in seconds?

This is the better way to write React code. This is what scales really well. This is what is super maintainable long term. Everything that doesn't work like this should smell to you.

But how do we get there? How do we make it so that components are self-contained systems? Especially when we have state that needs to be shared across those components?

Well we can start by splitting up the idea of a global "store" for all of our app's state. We need to be able to grab just the state we need when we need it. So instead of having one "store" we have many "containers".

We should also take this as an opportunity to consider the kinds of state that we have in our app. Which are vaguely these sorts of things:

  • UI State (selected tab, modal open/closed, etc)
  • Navigation State (current route, route values, etc)
  • Serializable UI (Form) State (forms, drag and drop, etc)
  • Remote Data State (HTTP APIs, Websockets, etc)

Each of these have very different implications.

  • Some are persisted, some disappear as soon as the component is removed.
  • Some we want to share state globally, some want to be re-initiated for every component instance.
  • Some need to track user interactions, some are synced to the server.
  • Some depend on browser/device state, some

However, when you have the same kind of state, it tends to work in the same exact way as it does everywhere else. For example, once you've written one form, every other form is going to be pretty similar structure wise; Once you've subscribed to a websocket in one place, every other websocket is going to be pretty similar.

This means that each of these kinds of states are great candidates for generic libraries that deal with them specifically, and have carefully designed APIs to solve the problems for that kind of state generically.

For example:

  • Navigation State -> @reach/router / react-router
  • Form State -> (react-)final-form / react-jeff
  • Websockets -> example
  • etc

Problem after problem, we can tackle our state management in pieces such that we have libraries that are carefully designed to solve the problem really well. Making full use of React along the way is part of it.

Once we've solved enough of these problems in a generic way, you start noticing that there's very little state left over. Once you've got solutions for requesting data from the server, for handling your forms, components for all your UI state, etc-- you've covered almost everything.

It's at that point that building UIs becomes a matter of just linking things together and moving things around. React Components, Context, and now Hooks are the three things you need to move those things around.

Going back to my original example:

import React from "react"
import { render } from "react-dom"
import { Router } from "@reach/router"

let Home = React.lazy(() => import("./Home"))
let About = React.lazy(() => import("./About"))

function Routes() {
  return (
    <Router>
      <Home path="/"/>
      <About path="/about"/>
    </Router>
  )
}

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

Of course, sometimes there are things that need to be shared between your entire app. Sometimes you will put context providers above your <Routes/>. But you should be aware of each one, and you should know exactly why it needs to be there.

function Routes() {
  return (
    <Intl.Provider>
      <Theme.Provider>
        <Router>
          <Home path="/"/>
          <About path="/about"/>
        </Router>
      </Theme.Provider>
    </Intl.Provider>
  )
}

Even further, you should always keep these things within a component, and be careful how you do. Because you should be able to move them down the component tree later on if you decide.

PR # 2: Add Dashboard

Note: I also moved the <Theme.Provider/> into the <Dashboard/> component because it's the only place we're ever going to use it.

  function Routes() {
    return (
      <Intl.Provider>
-       <Theme.Provider>
          <Router>
            <Home path="/"/>
            <About path="/about"/>
+           <Dashboard path="/dashboard"/>
          </Router>
-       </Theme.Provider>
      </Intl.Provider>
    )
  }

I just want to close by saying that React is really great at this stuff if you actually try to leverage it.

I think Redux sent the community down a path that was more comfortable for people who were coming from an MVC world and were scared of the number of responsibilities we were giving "components" in React. I think Redux stunted the community in that way.

I would challenge the idea that Redux scales well, I've seen it used on codebases with millions of lines of code, and it was a regular source of confusion for developers at all levels.

React on its own, used without heavy handed abstractions, scales quite beautifully, and you should try making better use of it.


I'm going to close this issue, because I don't think there is a way of exposing state/logic back out of React without compromising what I've outlined above. I would encourage you not to write the library off on that basis and to explore how you can better use React.

@raymondsze
Copy link
Author

raymondsze commented May 18, 2019

Thanks for the reply. I like the way how React hooks did.
Let's say we would like to integrate with Next.js, the getInitialProps is not part of React.js, it is not possible to use the same container among server side and client side right now, what we could do instead is pre-caclulate the initial state and prevent use it on server side.

I know it is not the responsibility of this library, but this could be done before with unstated.

I understand what you say and actually this is only true if everthing is React. With unstated or redux, we could have a store sharing between legacy jquery component and React Component.

Sometimes we may still want store living outside any React component like what redux do, but I am not saying we should put all the things like that.

Just want you to clarify the difference between unstated and unstated-next (especially what can/cant do before and what can/cant do now).

the next.js with-unstated (from offical repo) example is still using the way that server will create a container itself and call the function container have to replace the state, thats could introduce a misconception container should be used outside React Component. However, with unstated-next, we cannot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants