Coder Social home page Coder Social logo

react-copy-write's Introduction

react-copy-write

goat

An immutable React state management library with a simple mutable API, memoized selectors, and structural sharing. Powered by Immer.

Overview

The benefits of immutable state are clear, but maintaining that immutable state can sometimes be burdensome and verbose: updating a value more than one or two levels deep in your state tree can require lots of object/array spreading, and it's relatively easy to accidentally mutate something.

react-copy-write lets you use straightforward mutations to update an immutable state tree, thanks to Immer. Since Immer uses the copy-on-write technique to update immutable values, we get the benefits of structural sharing and memoization. This means react-copy-write not only lets you use simple mutations to update state, but it's also very efficient about re-rendering.

Documentation

react-copy-write is currently under-going significant API changes as it's tested in a production environment. Most documentation has been removed until we arrive at a stable API. Below you will find a bare-bones API reference that should get you started.

createState

The default export of the package. Takes in an initial state object and returns a collection of components and methods for reading, rendering, and updating state.

import createState from 'react-copy-write'

const {
  Provider,
  Consumer,
  createSelector,
  mutate,
} = createState({name: 'Brandon' });

Provider

The Provider component provides state to all the consumers. All Consumer instances associated with a given provider must be rendered as children of the Provider.

const App = () => (
  <Provider>
    <AppBody />
  </Provider>
)

If you need to initialize state from props you can use the initialState prop to do so. Note that it only initializes state, updating initialState will have no effect.

const App = ({user}) => (
  <Provider initialState={{name: user.name }}>
    <AppBody />
  </Provider>
)

Consumer

A Consumer lets you consume some set of state. It uses a render prop as a child for accessing and rendering state. This is identical to the React Context Consumer API.

const Avatar = () => (
  <Consumer>
   {state => (
     <img src={state.user.avatar.src} />
   )}
  </Consumer>
)

The render callback is always called with a tuple of the observed state, using an array. By default that tuple contains one element: the entire state tree.

Selecting State

If a Consumer observes the entire state tree then it will update anytime any value in state changes. This is usually not what you want. You can use the select prop to select a set of values from state that a Consumer depends on.

const Avatar = () => (
  <Consumer select={[state => state.user.avatar.src]}>
    {src => <img src={src} />}
  </Consumer>
)

Now the Avatar component will only re-render if state.user.avatar.src changes. If a component depends on multiple state values you can just pass in more selectors.

const Avatar = () => (
  <Consumer select={[
    state => state.user.avatar.src,
    state => state.theme.avatar,
  ]}>
    {(src, avatarTheme) => <img src={src} style={avatarTheme} />}
  </Consumer>
)

Updating State

createState also returns a mutate function that you can use to make state updates.

const {mutate, Consumer, Provider} = createState({...})

Mutate takes a single function as an argument, which will be passed a "draft" of the current state. This draft is a mutable copy that you can edit directly with simple mutations

const addTodo = todo => {
  mutate(draft => {
    draft.todos.push(todo);
  })
}

You don't have to worry about creating new objects or arrays if you're only updating a single item or property.

const updateUserName = (id, name) => {
  mutate(draft => {
    // No object spread required 😍
    draft.users[id].name = name;
    draft.users[id].lastUpdate = Date.now();
  })
}

Check out the Immer docs for more information.

Since mutate is returned by createState you can call it anywhere. If you've used Redux you can think of it like dispatch in that sense.

Optimized Selectors

createState also returns a createSelector function which you can use to create an optimized selector. This selector should be defined outside of render, and ideally be something you use across multiple components.

const selectAvatar = createSelector(state => state.user.avatar.src);

You can get some really, really nice speed if you use this and follow a few rules:

Don't call createSelector in render.

🚫

const App = () => (
  // Don't do this 
  <Consumer select={[createSelector(state => state.user)]}>
    {...}
  </Consumer>
)

👍

// Define it outside of render!
const selectUser = createSelector(state => state.user);
const App = () => (
  <Consumer select={[selectUser]}>
    {...}
  </Consumer>
)

Avoid mixing optimized and un-optimized selectors

🚫

const selectUser = createSelector(state => state.user);
const App = () => (
  // This isn't terrible but the consumer gets de-optimized so
  // try to avoid it
  <Consumer select={[selectUser, state => state.theme]}>
    {...}
  </Consumer>
)

👍

const selectUser = createSelector(state => state.user);
const selectTheme = createSelector(state => state.theme);
const App = () => (
  <Consumer select={[selectUser, selectTheme]}>
    {...}
  </Consumer>
)

react-copy-write's People

Contributors

andarist avatar aweary avatar chentsulin avatar eventualbuddha avatar futurepaul avatar mwilc0x avatar pedronauck avatar shengmin 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

react-copy-write's Issues

Expose state outside of Consumer & mutate

One thing I've often found myself wanting to do is access the store's state outside of React, or at least outside the scope of a consumer.

One option is mutate, however that places you into callback hell. My current preferred workaround is something like this:

const getState = () => new Promise((res) => {
  mutate((_, state) => {
    res(state);
  });
});

// ... inside an async function

const { myStoreProperty } = await getState();

However this isn't perfect either as it needlessly forces you into Promise-land.

Would it be possible to expose a function which just returns the current state - non-reactively of course - similar to the second argument of the mutate callback?

Alternatively, if I'm pursuing an anti-pattern with viable alternatives, I'd love to hear about that! 😄

There does appear to be precedent for this in Redux.

Thanks!

Replacing `this.setState()`?

Is it possible to use react-copy-write to read/write local component state rather than from the Provider context?

A possible API might look like this?

import { createLocalState } = from "react-copy-write";

const { LocalState, localMutate } = createLocalState({
  user: {
    avatar: {
      src: 'my-avatar.png'
    }
  },
  theme: {
    avatar: {
      background: 'blue'
    }
  }
});

const Avatar = ({ size }) => (
  <LocalState consume={{size}} select={[
    state => state.user.avatar.src,
    state => state.theme.avatar,
  ]}>
    {(src, avatarTheme) => (
      <img
        onClick={() => localMutate(draft => {
          // Mutate state! You dont have to worry about it!
          draft.avatarClickCount++;
        })}
        className={`avatar-${size}`}
        src={src}
        style={avatarTheme} />
    )}
  </LocalState>
)

Ideas for middleware.

Kudos to you for making this lib! I'm loving the mutating abilities.
I want to talk about having some middleware like abilities or hooks to the update function to implement common functionality among all mutations.
Two use cases that I wanted particularly were

  1. Logger/debug tool
  2. Crash reporter.

Did you have any plans of supporting such a thing? Would you be interested in a PR if I worked on it?

Create standalone documentation

The current README isn't great. I'd love to have an actual website for documentation with examples, tutorials, API reference, etc.

I'm leaning towards using https://docusaurus.io/. It looks easy to setup, and it's a Facebook product so dog-fooding is a plus.

[rcw] Proposal to add a 2nd optional parameter to `mutate`

Just to continue our discussion around adding a 2nd optional parameter to mutate to pass additional information/options that might be needed by different 'plugins' (eg. devtools). I'm proposing the following (basically allow each plugin to reserve one slot in the option object):

type Option = {
  // ... core options
  // plugin options
  pluginA?: {
     isDevMode: boolean,
  },
  pluginB?: {
    mutationName: string,
  },
};

// Could also use Symbol to reserve the slot when flow supports it

createSelector support multiple selectors

Feature Request

Similar to reselect, would be nice to compose selectors with other selectors, that way the business logic can be abstracted and isolated to the selector.

Example

// basic example: state.user.avatar.src
const user = createSelector(state => state.user);
const userAvatar = createSelector(user, userState => userState.avatar);
const userAvatarSrc = createSelector(userAvatar, userAvatarState => userAvatarState.src);

// advanced example: state.user.avatar.src || state.config.defaultAvatar
const config = createSelector(state => state.config);
const defaultAvatarSrc = createSelector(config, configState => configState.defaultAvatar);
const avatar = createSelector(
  userAvatarSrc,
  defaultAvatarSrc,
  (userAvatarSrcState, defaultAvatarSrcState) => (
    userAvatarSrcState || defaultAvatarSrcState
  )
);

Make updateState available in constructor instead of componentDidMount

Hey there. We recently started using react-copy-write in a small application and had a great experience so far, but there is still something that interferes with our application flow, which is the inability to call mutate before a provider is fully mounted.

https://codesandbox.io/s/oowloz2qk9

In this example the app will crash, because mutate is called before the surrounding provider is fully mounted. We want our ApiProvider to be wrapped around the whole app while still being able to use life cycle hooks to do initialization work that could mutate the state this provider gives us.

What we did for experimental reasons is to move the componentDidMount code from CopyOnWriteStoreProvider to a constructor and everything worked fine afterwards. So my question is whether it's possible to do this switch or if there are special reasons for the design decision to make updateState only available after componentDidMount of CopyOnWriteStoreProvider ran? Are there any performance implications or other pitfalls we are overlooking? If not I'd be glad to open a PR for this issue.

share state instance in multiple components

Hi,
what is the recommended way to create the state and re-use it across components in different files?
Imagine in your app.js you create the state

const State = createState({
  user: null,
  loggedIn: false
});

How do I use State and Providers in connected components?

I worked on a lib called statty and one of the most annoying thing is to create a wrapper component when you need the update in lifecycle hooks

  <State
    select={selector}
    render={({ count }, update) => (
      <ComponentsThatNeedsToAccessUpdateInLifecycle update={update} />
    )}
  />

createMutator seems meant to solve this pain, and I wonder what is the suggested way to use it in multiple components :)

Thanks

TypeScript definitions

Would you be willing to include a TypeScript declaration file in this repo and publish it as part of the package? I'm interested in using this library in a TypeScript project and would be willing to work on a PR that uses the existing Flow types as the basis for the TS definitions.

async mutators

If createMutator passed the mutate() method instead of the draft it could better support async

const { createMutator } = createState({
  value: "",
  loading: false,
  error: null,
});

const updateValue = createMutator(async (mutate, value) => {
  mutate(draft => { draft.loading = true; });
  try {
    await fetch('/api/v1/value', { method: 'POST', ... });
    mutate(draft => { draft.value = value; });
  } catch (error) {
    mutate(draft => { draft.error = error; });
  } finally {
    mutate(draft => { draft.loading = false; });
  }
});

Unable to use in a new React Native project

When importing react-copy-write in a React Native project it complains about it using Babel 7. I'm not sure why that would matter, but react-copy-write should work with RN out of the box.

Better selectors

Everything is awesome, except selectors. They are just ordinal selectors, and does not working as usual.

But, as long you are using immer to create a state, you can use the same Proxy-backed magic to select something from a state.

I am talking about memoize-state(https://github.com/theKashey/memoize-state), or react-memoize(https://github.com/theKashey/react-memoize) as example of memoize-state usage.

The first one was created to replace reselect, the second one - to provide memoization, selectors for context or memoized composition to react level.
You can double check react-memoize sources - it is literally one line. You can do the same.

Then

const UserPosts = ({ userId }) => (
  <State.Consumer
    selector={state => state.posts.filter(post => post.id === userId)}
  >
    {userPosts => userPosts.map(post => <Post {...post} />)}
  </State.Consumer>
);

will work out of the box. You need just to memoize selector before use.

state = {
   selector: memoizeState(this.props.selector)
 };

And then use memoized selector to actually caclucate things you need.
The solution is IE11 and ReactNative compatible, and quite fast.
More about - https://itnext.io/how-i-wrote-the-worlds-fastest-react-memoization-library-535f89fc4a17

Ready to answer any questions. Lets make this word a bit better.
PS: Not "just opening" PR without a green light.

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.