GithubHelp home page GithubHelp logo

diegohaz / constate Goto Github PK

View Code? Open in Web Editor NEW
3.9K 30.0 88.0 1.96 MB

React Context + State

Home Page: https://codesandbox.io/s/github/diegohaz/constate/tree/master/examples/counter

License: MIT License

JavaScript 28.27% TypeScript 71.73%
react react-context react-state state-management constate reactjs reakit react-hooks hooks

constate's Introduction

Hi there 👋

I'll eventually add something here.

constate's People

Contributors

codyaverett avatar dependabot[bot] avatar diegohaz avatar foray1010 avatar jamesplease avatar matheus1lva avatar mikestopcontinues avatar monkindey avatar panjiesw avatar samantha-wong avatar timkindberg avatar zheeeng 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

constate's Issues

Suggestion for API Tweak

What do you think of this API? It's a very simple change to the library, but personally I find it more elegant.

import React, { useState, useContext } from "react";
import createContainer from "constate";

// 1️⃣ Create a custom hook as usual
const useCounter = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(prevCount => prevCount + 1);
  return { count, increment };
}

// 2️⃣ When you need to share your state, simply wrap your hook
//    with the createContainer higher-order hook, like so:
const useCounter = createContainer(() => {
   // ... same logic here
});

function Button() {
  // 3️⃣ Use the same hook like you were before, with the same api. No change.
  const { increment } = useCounter()
  return <button onClick={increment}>+</button>;
}

function Count() {
  // 4️⃣ But now you can use it in other components as well.
  const { count } = useCounter()
  return <span>{count}</span>;
}

function App() {
  // 5️⃣ The caveat, you wrap your components with the Provider that is
  //    attached to the hook
  return (
    <useCounter.Provider>
      <Count />
      <Button />
    </useCounter.Provider>
  );
}

The change in the code is roughly this (the last 3 lines specifically):

export function createContainer(useValue, createMemoInputs) {
  const Context = React.createContext()

  const Provider = props => {
    const value = useValue(props)
    const { children } = props
    const memoizedValue = createMemoInputs ? React.useMemo(() => value, createMemoInputs(value)) : value
    return <Context.Provider value={memoizedValue}>{children}</Context.Provider>
  }

  const hook = () => useContext(Context)
  hook.Provider = Provider
  return hook
}

Lazy Initialization doesn't work in useContextState

Note: this issue refers to v1 alpha

I saw that you wraps useContextState inside a useContextReducer, but this has a problem. `React.useState and React.useReducer handle lazy initialization differently.

React.useState accept initialState as a function to be computed once, but React.useReducer doesn't. It uses a initialAction argument instead for perform lazy initialization.

Because of this, it's currently not working, since initialState argument is passed to React.useReducer that does not handle initialState as function.

useContextState('context-state', () => someExpensiveComputation(props))

Pass state to setState callback

Since effects and lifecycles have no way to access the updated state in setState callbacks (using this.state), we should provide it.

const increment = () => ({ setState }) => {
  setState(state => ({ count: state.count + 1 }), newState => ...)
}

Another option would be deprecate state prop in favor of a new getState.

mount

import { mount } from "constate";
import CounterContainer from "./CounterContainer";

const counter = mount(CounterContainer);

expect(counter.count).toBe(0);

counter.increment(10);

expect(counter.count).toBe(10);

HOC?

Do you plan creating a HOC like connect for like dispatching actions on componentDidMount and having state available on those steps

Side effects / async actions

I'm thinking on a way to handle side effects and/or async actions on constate. This is how I'm currently planning to use it:

const effects = {
  createPost: payload => async ({ state, setState, props }) => {
    try {
      const data = await api.post('/posts', payload)
      setState({ data })
    } catch (error) {
      setState({ error })
    }
  }
}

Possible names:

  • asyncActions
  • effects
  • thunks
  • methods
  • (suggestions are welcome)

RFC: Hooks

React hooks were announced this week and will be available in React 16.7. I'm opening this issue to talk about an addition (or maybe a replacement) on this library's API.

Purpose of this library

First, it's important to clarify the purpose of this library. A big part of the state of our applications should be local, and some should be shared, contextual or global (whatever people call it). But we don't always know whether a state should be local or shared when first writing it.

Having to refactor local state into Context or Redux later could be cumbersome. Many choose to write global state (using Redux, MobX, React Context etc.) upfront just because some day they may need it.

Constate makes it easier to write local state and make it global later.

And that's why it's called Constate. Context + State.

UPDATE: Final API

Provider

Provides state to its children;

import { Provider } from "constate";

function App() {
  return (
    <Provider devtools>
      <Counter />
    </Provider>
  );
}

useContextState

Same as React's useState, but accepting a context key argument so it accesses the shared state on Provider.

import { useContextState } from "constate";

function Counter() {
  const [count, setCount] = useContextState("counter1", 0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

useContextReducer will be available as well.

useContextEffect

Effect that runs on Provider so it will run only once per context key, not per consumer.

import { useContextState, useContextEffect } from "constate";

function Counter() {
  const [count, setCount] = useContextState("counter1", 0);
  
  useContextEffect("counter1", () => {
    document.title = count;
  }, [count]);

  return <div>{count}</div>;
}

Other methods such as useContextMutationEffect and useContextLayoutEffect will be available as well.

Context

The Context object will be exposed so users will be able to useContext directly to access the whole shared state tree:

import { useContext } from "react";
import { Context } from "constate";

function Counter() {
  const [state, setState] = useContext(Context);
  const incrementCounter1 = () => setState({
    ...state,
    counter1: state.counter1 + 1
  });
  return <button onClick={incrementCounter1}>{state.counter1}</button>;
}

createContext

Alternatively, a new context object can be created:

// context.js
import { createContext } from "constate";

const {
  Context,
  Provider,
  useContextState,
  useContextEffect
} = createContext({ counter1: 0 });

export {
  Context,
  Provider,
  useContextState,
  useContextEffect
};
import { useContextState } from "./context";

function Counter() {
  const [count, setCount] = useContextState("counter1");
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Alternative 1: useState / useEffect

Given the following example using normal React useState:

import { useState } from "react";

const CounterButton = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return <button onClick={increment}>{count}</button>;
};

Say we want to share that count state between multiple components. My idea is to export an alternative useState method so people can pass an additional context argument to access a shared state.

import React from "react";
// import `useState` from constate instead of react
import { Provider, useState } from "constate";

const CounterButton = () => {
  // the only difference here is the additional argument
  const [count, setCount] = useState(0, "counter1");
  const increment = () => setCount(count + 1);
  return <button onClick={increment}>{count}</button>;
};

// Another component that accesses the same shared state
const CounterText = () => {
  const [count] = useState(0, "counter1");
  return <p>You clicked {count} times</p>;
};

const App = () => (
  <Provider devtools>
    <CounterText />
    <CounterButton />
  </Provider>
);

Also, it could be abstracted so as to provide something similar to what we have today:

// useCounter.js
import { useState } from "constate";

function useCounter({ initialState, context } = {}) {
  const [state, setState] = useState({ count: 0, ...initialState }, context);
  const increment = () => setState(prevState => ({ 
    ...prevState, 
    count: prevState.count + 1 
  });
  return { ...state, increment };
}

export default useCounter;
// Counter.js
import React from "react";
import useCounter from "./useCounter";

const Counter = () => {
  const { count, increment } = useCounter({ context: "counter1" });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Click me</button>
    </div>
  );
}

Currently, we have some lifecycle props such as onMount, onUpdate and onUnmount. They serve not only as a nicer way to use lifecycles within functional components, but they also guarantee that they will trigger per context (if the prop is passed), and not per component.

Now we can use lifecycles within functions with useEffect, so Constate doesn't have to intrude on it anymore. But the second problem still persists.

As an example, consider a shared state that would fetch some data from the server in componentDidMount. If the state is shared, this should be triggered only once (when the first component gets mounted, for example), and not once per component that accesses that state.

I'm still not sure how to make it work. Maybe we would need to export an alternative useEffect to do the job.

Alternative 2: useConstate/useConstateEffect

Instead of useState and useEffect, we can just export useConstate and useConstateEffect:

import { useConstate, useConstateEffect } from "constate";

const Counter = () => {
  const context = "counter1";
  const [count, setCount] = useConstate({ initialState: 0, context });

  useConstateEffect({
    create: () => {
      document.title = count;
    },
    inputs: [count],
    context
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Click me</button>
    </div>
  );
};

That's because the signatures can't be the same. When using context, most of the time we won't set initialState. Having it as the first argument would require us to write useState(undefined, "context") all the time, which can be cumbersome.

An object parameter solves this problem. The keys follow the parameter names of the original methods on React codebase.

Alternative 3: useConstate

import { useConstate } from "constate";

const Counter = () => {
  const { useState, useEffect } = useConstate("counter1");
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  useEffect(() => {
    document.title = count;
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Click me</button>
    </div>
  );
};
- import { useState, useEffect } from "react";
+ import { useConstate } from "constate";

  const Counter = () => {
+   const { useState, useEffect } = useConstate("counter1");
    const [count, setCount] = useState(0);

    const increment = () => setCount(count + 1);

    useEffect(() => {
      document.title = count;
    }, [count]);

    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={increment}>Click me</button>
      </div>
    );
  };

Alternative 4: useContextState/useContextEffect

import { useContextState, useContextEffect } from "constate";

const Counter = () => {
  const [count, setCount] = useContextState("counter1", 0);

  const increment = () => setCount(count + 1);

  useContextEffect(
    "counter1", 
    () => {
      document.title = count;
    }, 
    [count]
  );

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Click me</button>
    </div>
  );
};

Use case of selectors

(I was writing some abstractions with the same idea and came across your project, thanks for sharing)

Why do we need selectors? Couldn't we just use a normal function since any state property is available to the render function? <button onClick={() => increment(1)}>{count} {getParity(count)}</button>

Reqeust: with(apply)Provider(Container) HOC

Hello there, I think we still need a helper HOC for applying the container provider named withProvider/withContainer/withContainerProvider or whatever. Otherwise, we always have to modify some component's parent to add such codes <Container.Provider>....</Container.Provider> and when we decide to remove a context we have to modify its parent again and again.
I have a request that adding the HOC helper:

import React, { useState, useContext, useEffect } from "react";
import createContaine, { withProvider }r from "constate";
import {userCounter, MainCounter} from '../containers/counter'
import { Button, Count } from '../components'

// we wish use context inside the DynamicImportedRoutePage(current componet),
// not only its descendants
function DynamicImportedRoutePage() {
   // we may use count context here
  const { increment } = useContext(MainCounter.Context);

    useEffect(
       () => {
           increment()
       },
       []
     )

  return (
    <>
      <Count />
      <Button />
    </>
  );
}

// Use the HOC
export default withProvider(MainCounter)(DynamicImportedRoutePage)

When we decide to refactor the DynamicImportedRoutePage, just remove/change the HOC and update the function body

Testing components

How can you test a component that uses a Constate Container?

I've found some issues testing child-as-function with Enzyme. Do you have any strategy for it?

Warn about ignored initialState

Since the last version, when a Container has context, we're ignoring initialState after it's been set. We should warn when someone tries to set further initialState.

minor feedback

(following up on your tweet)

to simplify things further, I'd say that the Container terminology is unnecessary and the API would be nicer if it aligned with React's default context/hook API

so have createContext (rather than createContainer) return { Provider, Consumer}, and pass it to the hook as useContext(MyContext)

in other words, constate is just a tiny wrapper around context to faciliate usage n improve performance without a custom API (at least this I'm thinking of doing in my app, using your code as an example, so thanks!)

Preventing components from being recalled

Component which are using the context provider will recalled on every state change. I’m aware that only DOM elements that really changed will get re-rendered but that is still a problematic issue because many components often have functions in them and they will be re-executed.

Did you tackle this issue?

No cache in selectors, <Container /> performance issues in huge projects

Hi,

as I can see in:
https://github.com/diegohaz/constate/blob/master/src/Container.tsx#L152

there is no cache for selectors and every time is rendered it maps actions, effect, selectors. I think that large amount of actions in bigger projects and mapping them inside each <Container> render cycle will dramatically decrease overall app performance.

So question is:

  • selector cache will be available in future releases?
  • method mapping will be moved to something that is parent?

For now - project is unusable for project that are larger than Todo app.

withContainer HOC

const CounterContainer = props => (
  <Container
    initialState={{ count: 0 }}
    actions={{ increment: () => state => ({ count: state.count + 1 }) }}
    {...props}
  />
);

const withContainer = (Container, containerProps) => WrappedComponent => props => (
  <Container {...containerProps}>
    {state => <WrappedComponent {...props} {...state} />}
  </Container>
);

const Counter = ({ count, increment }) => (
  <button onClick={increment}>{count}</button>
);

export default withContainer(CounterContainer)(Counter);

Compose Containers

const TodosContainer = props => (
  <Container initialState={{ todos: [] }}  />
);

const VisibilityContainer = props => (
  <Container initialState={{ visibleItems: [] }} />
);

<Container compose={[TodosContainer, VisibilityContainer]}>
  {({ todos, visibleItems }) => ...}
</Container>

eslint warning: 'React.useMemo' is called conditionally

after installing 'eslint-plugin-react-hooks', a warning is reported saying:
'React.useMemo' is called conditionally
in this line,

const memoizedValue = createMemoInputs
      ? React.useMemo(() => value, createMemoInputs(value))
      : value;

Passing `createMemoDeps` as the second argument is deprecated

Solution:

import { useState, useMemo } from "react";
import createUseContext from "constate";

function useCounter() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount(count + 1);
  return { count, increment };
}

// const useCounterContext = createUseContext(useCounter, value => [value.count]);
const useCounterContext = createUseContext(() => {
  const value = useCounter();
  return useMemo(() => value, [value.count]);
});

Pass type to onUpdate

const onUpdate = ({ prevState, state, setState, type }) => {
  // type will be action name, effect name or lifecycle prop name
}

Can I update app state (global) from onMount in another component?

The following code will just update the component MyView's state. I want to update app's state, but how? If I put an onMount on the app container (context="app") it will never be run since it's not the first being called as by design (and in your documentation).

const onMount = async ({ state, setState }) => {
  try {
    doSomething()
  } catch (error) {
     setState({ showSnackbar: true, snackbarMessage: error.msg })
  }
}

const ConnectedMyView = props => (
  <Container context="myview" onMount={({ state, setState }) => onMount({ state, setState })}>
    {() => (
      <Container context="app">
        {({ foo }) => (
          <MyView foo={foo} {...props}/>
        )}
      </Container>
    )}
  </Container>
)

shouldUpdate

Do not render nor call onUpdate on setState({ interval }).

const initialState = { count: 0 };

const onMount = ({ setState }) => {
  const fn = () => setState(state => ({ count: state.count + 1 }));
  const interval = setInterval(fn, 1000);
  setState({ interval });
};

const onUnmount = ({ state }) => {
  clearInterval(state.interval);
};

const shouldUpdate = ({ state, nextState }) => state.interval === nextState.interval;

const Counter = () => (
  <Container
    initialState={initialState}
    onMount={onMount} 
    onUnmount={onUnmount}
    shouldUpdate={shouldUpdate}
  >
    {({ count }) => <button>{count}</button>}
  </Container>
);

Fallback to local state when there's no Provider

Thanks so much for constate - wonderful, indeed!

I'm using it in a rather large project and finding that it is tedious to wrap every test with a Provider. React's use of a default context value in createContext helps with this normally because then useContext will get the default, even if no provider is in the tree. Alas, you can't do that with constate because there's no way use the hook outside of a component.

I'd love to get your thoughts on this... What I'm thinking of is something like this (admittedly big) api change:

// OLD
const counter = useContext(CounterContainer.Context);

// NEW
const counter = CounterContainer.use();

The use() function does the useContext subscription, but also knows how to call the
underlying hook instead if the context is empty. This makes writing tests really easy - just like React's createContext behavior when no provider is present.

There are lots of (mostly unfortunate) issues with this proposal, tho:

  1. Since we use undefined in the context to signify the no-provider case, container hooks can never return undefined (seems pretty minor?). Easy to guard for that case.

  2. Since use() has to call the hook when no provider is given, container hooks cannot have
    props anymore
    (seems pretty major).

  3. It is a bit nasty to conditionally call the hook (use will call it if no provider is present, otherwise it won't). But this might not be a problem ITRW because you're either running real code (with a provider) or you're running tests (without a provider) and the render tree should be consistent across renders in each of those cases. React will complain if you get this wrong (like conditionally adding/removing providers, which seems super unlikely).

So it ends up like something like this:

function createContainer<V>(
  useValue: () => V,
  createMemoInputs?: (value: V) => any[]
) {
  const Context = createContext<V | undefined>(undefined);

  const Provider = (props: { children?: React.ReactNode }) => {
    const value = useValue();
    if (value === undefined) {
      throw new Error('Container hooks must return a value');
    }

    // createMemoInputs won't change between renders
    const memoizedValue = createMemoInputs
      ? React.useMemo(() => value, createMemoInputs(value))
      : value;
    return (
      <Context.Provider value={memoizedValue}>
        {props.children}
      </Context.Provider>
    );
  };

  const use = () => {
    let value = useContext(Context);
    if (value === undefined) {
      warnNoProvider();
      value = useValue();
    }
    return value;
  };

  return {
    use,
    Provider
  };
}

Add lifecycle props to Container

I'm having to use lifecycles often. I think it might be a good idea to implement something like https://github.com/reactions/component.

const increment = amount => state => ({ count: state.count + amount });

const CounterContainer = props => (
  <Container 
    initialState={{ count: 0 }}
    onMount={({ setState }) => setState(increment(10))} // componentDidMount
    onUpdate={({ setState }) => setState(increment(10))} // setState call
    onUnmount={({ setState }) => setState(increment(10))} // componentWillUnmount
    actions={{ increment }}
    {...props}
  />
);

Move type declarations to root dir

In current version (1.0.0) type declarations are published under dist/ts/src and when importing for example by the default VS Code action, the result is:

image

Besides unfancy look, it leads to error during compilation

Module not found: Error: Can't resolve 'constate/dist/ts/src' in 'C:\source\project\src'
...

[constate] Missing Provider

Hello.

I read in the documentation that

Constate exports a single method called createContainer. It receives two arguments: useValue and createInputs (optional). And returns { Context, Provider }.

So, without too much insight into hooks, I assumed that the Provider returned was interchangeable with Reacts Context.Provider. Therefor i tried using it as a static Class.contextType. -This does not work, as the Provider returned is just a proxy that warns "[constate] Missing Provider".

Reading the closed RFC: Hooks issue, I am wondering if what I am trying to achieve (working with a huge legacy code base), is doable with Constate?

I am sorry for the vague question, I am racing through my options due to timelimits, trying to gather as much intel as possible.

Anti-pattern question

Is it an anti-pattern to cache the setContextState function for subsequent use by a non-React-Context component?

For example, this approach appears to work in practice:

// Non-context module

export const onMount = ({ setContextState }) => {
  cachedSetContextState = setContextState;
};

Which can be registered with a <Provider>:

<Provider onMount={onMount} />

So that later on in the app the cached context state can be accessed to update the context state by a component that isn't otherwise bound into the React context:

// In the same non-context module as above
// Has access to the cached `cachedSetContextState` function
cachedSetContextState('context', { foo: 'bar' });

Is this a bad idea?

What I think I trying to achieve is something like channels in redux-saga, i.e. to be able to mutate the state from a non-GUI source.

child component is not rerendering.

So I have

import createContainer from "constate";
export const Form = createContainer(useFormState, value => [value.values]);
function formReducer(state, action) {
  switch (action.type) {
    case 'add-text':
      let newState = {
        ...state,
        report_fields: [...state.report_fields, {value:null, type:'text'}]
      }
      return newState;
    default:
      throw new Error();
  }
}

function useFormState({ initialValues = {} } = {}) {
  const [state, dispatch] = useReducer(formReducer, initialValues);
  return { state, dispatch };
}
<Form.Provider initialValues={{report_fields: props.report_fields, report: props.report, team: props.team}}>
   <Wizard/>
</Form.Provider>

The Wizard does this

function Wizard() {
  const { step, next, previous } = useContext(Step.Context);
  const steps = [StepOneQuestions, StepTwoMembers];
  return React.createElement(steps[step]);
}

And in StepTwo

export default function StepOneQuestions (props) {
    const {state, dispatch} = useContext(Form.Context);
    
    return ( .... )
}

That StepOneQuestions is not re-rendering when dispatching an action such as add-text

Build errors Unexpected token

Was going to go through some local dev cycles with npm link to further troubleshoot #38 and ran into some issues building constate locally.

Env
OSX 10.13.6
node v8.11.2
npm v6.4.0

Steps to reproduce
run npm i
run npm run build

Error Output


> [email protected] prebuild /directory/constate
> npm run clean


> [email protected] clean /directory/constate
> rimraf dist


> [email protected] build /directory/constate
> tsc --emitDeclarationOnly && rollup -c


src/index.ts → dist/constate.cjs.js, dist/constate.es.js...
(node:8176) Warning: N-API is an experimental feature and could change at any time.
[!] Error: Unexpected token
src/types.ts (3:7)
1: import * as React from "react";
2:
3: export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
          ^
4:
5: export type Dictionary<T> = { [key: string]: T };

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] build: `tsc --emitDeclarationOnly && rollup -c`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!    /directory/.npm/_logs/2018-08-28T19_41_13_671Z-debug.log

Starting to wonder if it's just me 😅

Issue rendering state changes to other components

I'm trying to utilize Constate to manage a Gatsby application.

I have my gastby-ssr.js and gatsby-browser.js files set up properly with a provider:

gatsby-ssr:

import React from "react";
import { Provider } from "constate";
import { renderToString } from "react-dom/server";

export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {
  const app = () => <Provider devtools>{bodyComponent}</Provider>;
  replaceBodyHTMLString(renderToString(<app />));
};

gastby-browser:

import React from 'react';
import { Provider } from 'constate';

export const ConnectedRouterWrapper = ({ children }) => {

<Provider devtools={true}>
    {children}
</Provider>
};

This is the container that I'm trying to use:

import * as React from 'react';
import { Container } from 'constate';

//declare and export the initial state for Constate to start with
export const initialState = {
  count: 0
};

//actions, or syncronous functions that will update the shared state
export const actions = {
  increment: amount => state => ({ count: (parseInt(state.count) + 1) })
};

//effects, or asyncronous functions that will update the shared state
export const effects = {
  // increment: amount => state => ({ count: state.count + amount })
};

//export the container itself
const MainContainer = (props) => {
  return <Container initialState={initialState} effects={effects} actions={actions} context="main" {...props} />;
};

export default MainContainer;

and here is how I'm rendering the "MainContainer" component:

  {/* Component 1 */}
<MainContainer context="main">
  {({ count, increment }) => (
    <button onClick={increment}>{count}</button>
  )}
</MainContainer>


{/* Component 2, which isn't updating */}
<MainContainer context="main">
{({ count, increment }) => (
  <button onClick={increment}>{count}</button>
)}
</MainContainer>

When I click on "Component 1" it calls the increment function properly, but "Component 2" isn't re-rendered with the updated state like "Component 1" is

If reading global state, is nesting containers needed to modify local state?

In my component I need to read global state (context="app") but I also need to update local state, what is the best practice for combining that using constate? Below is an example that isn't working but just to get you some kind of idea what I've tried. Is nesting containers needed here?
(I got another related question about onUnmount also but I think it's better to add that later to not make this more complex than necessary.)

updateFoobar is triggered in MyView and should update local state.

const initialState = {
  foobar: false,
}

const effects = {
  ...
}

const effectsLocal = {
  updateFoobar: value => ({ setState }) => {
    setState({foobar: value})
  },
}

const MyView = props => (
  <Container
    initialState={initialState}
    effects={effectsLocal}>
    {(foobar, updateFoobar) => (
      <Container context="app" effects={effects}>
        {({ globalVar, setState }) => {
          return (
            <MyView
              {...props}
              globalVar={globalVar}
              updateFoobar={(value) => updateFoobar(value)}
            />
          )
        }}
      </Container>
    )}
  </Container>
)

Pass props to effects and lifecycles

Also, props should behave consistently between Container and ContextContainer

const tick = ({ setState, props }) => {
  if (props.foo === "bar") {
    setState({ foo: "bar" });
  }
};

Performance impact: Not re-rendering a component when only calling a shared function

Could you elaborate and add an example in the documentation where we can prevent the components from rendering if they are only calling out the functions.

Going with the count and Increment examples.
It is ok to re-render a component which is using count and displaying it if count changes.
But should we really be re-rendering the component which is only calling the increment function. The component calling the function does not care what the new state is, all it is interested is in calling the function.

More examples on these lines would be great, since I could not find more documentation besides the example on the git page.

Great library and idea. This is exactly what I was looking for, ability to create hooks and then share state globally using context !!! I started doing the work and mid way found your library, so much appreciate it.

Use with preact

this package exports React.FunctionComponent type, but preact has different type

I found no issues running this package with preact (without actually running :) ), but it's nearly impossible to do with TypeScript due to typings

image

Provide "name" option for easier debugging

Currently if you create a hook with this library and debug it with the React Devtools, the Provider is just called Provider and the Context is just called Context (The screenshot is from the CodeSandbox example). This can be confusing if you have many context hooks.

image

I propose an option, which allows the user to define a name. This name could be used to prefix the Provider and Context. Example:

const useCounterContext = createUseContext(useCounter, { name: 'counter' });

In this case the name of the Provider could be CounterProvider and of the Context CounterContext (by using displayName in the library).

If you are interested, I could create a PR for this.

this is more of a question then an actual issue regarding performace

i see that like any usage of contextAPI, each child that is under the provider is getting rendered.
i found that if i wrap the functions that are under that provider with React.memo it will prevent unneeded renders

was wondering what you think about this, did constate handle this issue? i could not find in the docs.

This is the code change:

const Button = React.memo(() => {
  // 3️⃣ Use container context instead of custom hook
  // const { increment } = useCounter();
  const { increment } = useContext(CounterContainer.Context);
  return <button onClick={increment}>+</button>;
})

const Count() = React.memo(() => {
  // 4️⃣ Use container context in other components
  // const { count } = useCounter();
  const { count } = useContext(CounterContainer.Context);
  return <span>{count}</span>;
}

Context and Server Side Rendering

One of our project's GatsbyJS server side rendering seems to have some issues with finding the shape of state during build.

Example Snippet

export const SharedContainer: ComposableContainer<State, Actions, {}, Effects> = props => {
    return <Container initialState={initialState} effects={effects} actions={actions} context="myContextString" {...props} />;
};

Gatsby Build error

error Building static HTML for pages failed

See our docs page on debugging HTML builds for help https://goo.gl/yL9lND

  302 |             mountContainer = _ref.mountContainer;
  303 |         return createElement(InnerContainer, _extends({}, _this4.props, {
> 304 |           state: state[context],
      | ^
  305 |           setContextState: setContextState,
  306 |           mountContainer: mountContainer
  307 |         }));


  WebpackError: TypeError: Cannot read property 'myContextString' of undefined

The link in the error describes common pitfalls of SSR in gatsby.
e.g. window object is missing, either mockup window or wrap the code in a conditional so that it only runs if the object is truthy.

Running the app in dev mode does not have this issue. (Not SSR)

I'm trying to figure out how to get around this. I'd like to not have to mock out the state of the app and just use context as is, but i'm not sure if that is possible at this point.

Accessing actions, effects and selectors from within

Currently the actions, effects, selectors and lifecycle methods like onMount, onUpdate and onUnmount only have access to state and setState.

Making bound versions of their siblings available within these functions would enable more use-cases and promote code re-use.

Example Use-case

The use-case below shows several interesting characteristics:

  • the request tracking code is encapsulated in the action and the selector
  • the initial loading can be triggered via the onMount handler
// one of the actions
const updateRequestStatus = (status: 'loading' | 'loaded' | 'failed') => (state: State) => ({
  ...state,
  requestHistory: [
    ...state.requestHistory,
    {
      status,
      time: Date.now(),
    },
  ],
});

// one of the selectors
const isLoading = () => ({ requestHistory }: State) => (
  requestHistory.length > 0
    ? requestHistory[requestHistory.length - 1].status === 'loading'
    : false
)

// one of the effects
const loadFromServer = (id: string) => ({ actions, selectors, state }) => {
  if (selectors.isLoading()) {
    return
  }

  actions.updateRequestStatus('loading');
  fetch(`${state.basepath}/my-rest-route/${id}`)
    .then(result => actions.updateRequestStatus('loaded'));
}

// the onMount handler
const onMount: OnMount<State> = ({ effects, state }) => {
  effects.loadFromServer(state.someId);
}

While the first aspect can be achieved in a different way (encapsulate in external update functions), triggering effects on mount currently requires many contortions to achieve.

Would you be interested in adding these features or would you accept PRs to that extent?

Remove mount from the main package

Since that's an experimental feature, it should be removed from the main package and imported like import mount from "constate/mount"

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.