Coder Social home page Coder Social logo

lostpebble / pullstate Goto Github PK

View Code? Open in Web Editor NEW
1.1K 14.0 23.0 4.45 MB

Simple state stores using immer and React hooks - re-use parts of your state by pulling it anywhere you like!

Home Page: https://lostpebble.github.io/pullstate

License: MIT License

TypeScript 91.45% JavaScript 7.50% Dockerfile 0.10% CSS 0.54% HTML 0.40%
react hooks reactjs immer state-management state server-side-rendering

pullstate's Introduction

Pullstate

pullstate

Ridiculously simple state stores with performant retrieval anywhere in your React tree using the wonderful concept of React hooks!

  • ~7KB minified and gzipped! (excluding Immer and React)
  • Built with Typescript, providing a great dev experience if you're using it too
  • Uses immer for state updates - easily and safely mutate your state directly!
  • NEW - Create async actions and use React hooks or <Suspense/> to have complete control over their UI states!

Originally inspired by the now seemingly abandoned library - bey. Although substantially different now- with Server-side rendering and Async Actions built in! Bey was in turn inspired by react-copy-write.

Try out a quick example:

Edit Pullstate Client-only Example


Let's dive right in

This is taken directly from the documentation site, to give you a quick overview of Pullstate here on github. Be sure to check out the site to learn more.

To start off, install pullstate.

yarn add pullstate

Create a store

Define the first state store, by passing an initial state to new Store():

import { Store } from "pullstate";

export const UIStore = new Store({
  isDarkMode: true,
});

Read our store's state

Then, in React, we can start using the state of that store using a simple hook useState():

import * as React from "react";
import { UIStore } from "./UIStore";

export const App = () => {
  const isDarkMode = UIStore.useState(s => s.isDarkMode);

  return (
    <div
      style={{
        background: isDarkMode ? "black" : "white",
        color: isDarkMode ? "white" : "black",
      }}>
      <h1>Hello Pullstate</h1>
    </div>
  );
};

The argument to useState() over here (s => s.isDarkMode), is a selection function that ensures we select only the state that we actually need for this component. This is a big performance booster, as we only listen for changes (and if changed, re-render the component) on the exact returned values - in this case, simply the value of isDarkMode.


Add interaction (update state)

Great, so we are able to pull our state from UIStore into our App. Now lets add some basic interaction with a <button>:

  return (
    <div
      style={{
        background: isDarkMode ? "black" : "white",
        color: isDarkMode ? "white" : "black",
      }}>
      <h1>Hello Pullstate</h1>
      <button
        onClick={() =>
          UIStore.update(s => {
            s.isDarkMode = !isDarkMode;
          })
        }>
        Toggle Dark Mode
      </button>
    </div>
  );

Notice how we call update() on UIStore, inside which we directly mutate the store's state. This is all thanks to the power of immer, which you can check out here.

Another pattern, which helps to illustrate this further, would be to actually define the action of toggling dark mode to a function on its own:

function toggleMode(s) {
  s.isDarkMode = !s.isDarkMode;
}

// ...in our <button> code
<button onClick={() => UIStore.update(toggleMode)}>Toggle Dark Mode</button>

Basically, to update our app's state all we need to do is create a function (inline arrow function or regular) which takes the current store's state and mutates it to whatever we'd like the next state to be.

Omnipresent state updating

Something interesting to notice at this point is that we are just importing UIStore directly and running update() on it:

import { UIStore } from "./UIStore";

// ...in our <button> code
<button onClick={() => UIStore.update(toggleMode)}>Toggle Dark Mode</button>

And our components are being updated accordingly. We have freed our app's state from the confines of the component! This is one of the main advantages of Pullstate - allowing us to separate our state concerns from being locked in at the component level and manage things easily at a more global level from which our components listen and react (through our useStoreState() hooks).

pullstate's People

Contributors

bitttttten avatar carlpaten avatar davej avatar dependabot[bot] avatar lostpebble avatar schummar avatar willstott101 avatar wnz99 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

pullstate's Issues

[Question] How to implement "click on a button then run an async action" in an elegant way?

I see the examples of async actions are about "running async actions when rendering a component, e.g. https://lostpebble.github.io/pullstate/docs/async-actions-creating

Now I have a different case: In a component, there is a button, when I click on it, it will run some async actions, and rendering different content base on the result of the async action.

I couldn't find an elegant way to implement it, hope to have your help, thanks!

This is a small demo: https://github.com/freewind-demos/typescript-react-pullstate-async-actions-demo

The RemoteHello1 there is what I want to do (in traditional way), and the RemoteHello2 is what I want to use pullstate's async actions but failed.

Question about "target": "esnext" in tsconfig.json

I see in the tsconfig.json, we have:

"compilerOptions": {
    // ...
    "target": "esnext",
}

Just wondering why use esnext as target instead of es5/es6, as the later one can generate safer syntax for some old browsers like IE?

I have this question because I found my app using pullstate can't run on IE11 directly (sadly we have to support it), because there are some syntax is not supported on IE 11, (e.g. const [a,b] = ...), so we have to configure our webpack and babel to convert node_modules/pullstate files

Infinite loop on async action run

When an async action has a cacheBreakHook, calling run on the action can cause an infinite loop.

The action:

import { createAsyncAction, successResult } from "pullstate";

export interface GetPowParams {
  value: number;
}

export const GetPow = createAsyncAction(
  async ({ value }: GetPowParams) => {
    console.log("GetPow running...");
    await new Promise<void>((resolve, reject) => setTimeout(resolve, 1000));
    const result = value * value;
    console.log("GetPow result:", result);
    return successResult(result);
  },
  {
    cacheBreakHook: ({ timeCached }) => timeCached + 10000 < Date.now() // Comment this line out to fix the issue
  }
);

The app:

import React, { Fragment } from "react";
import { GetPow } from "./GetPow";

export function AppComp() {
  const [isLoaded, response] = GetPow.useBeckon({ value: 4 });
  return (
    <Fragment>
      <p>Open the console.</p>
      <p>Then press the โ€˜Runโ€™ button to get an infinite loop.</p>
      {!isLoaded ? (
        <div>Calculating...</div>
      ) : response.error ? (
        <div>{response.message}</div>
      ) : (
        <div>Pow: {response.payload}</div>
      )}
      <div>
        <button
          type="button"
          onClick={() => {
            console.log('Clicked Run')
            GetPow.run({ value: 4 });
          }}
        >
          Run
        </button>
      </div>
    </Fragment>
  );
}

Expected output:

GetPow running...
GetPow result: 16
Clicked Run
GetPow running...
GetPow result: 16

Actual output:

GetPow running...
GetPow result: 16
Clicked Run
GetPow running...
GetPow running...
GetPow result: 16
GetPow running...
GetPow result: 16
GetPow running...
GetPow result: 16
... repeating forever ...

Here is a simple project to reproduce the issue.

Dynamic getSubState in useStoreState

I have a case where I want to get a list of data based on some prop.

const myComponent = ({type}) => {
  let data = useStoreState(store, (s) => s[type]);
  // do stuff with data
}

However, the selector is not updating when I change the type prop. Is there any way to make this work?

using pullstate as localState

One issue I find with using useState is that it causes re-rendering of the entire component tree. Using local pullstate would solve this. Here is an example:

import React, { useState } from "react";
import { Store } from "pullstate";

const createPageStore = () => {
  console.log("Page Store created");
  return new Store({
    items: [1, 2, 3, 4, 5],
    selectedId: undefined,
    itemsById: {
      1: { name: "One" },
      2: { name: "Two" },
      3: { name: "Three" },
      4: { name: "Four" },
      5: { name: "Five" }
    }
  });
};

function ListItem({ pageStore, id }) {
  const item = pageStore.useState((s) => s.itemsById[id], [id]);
  const selected = pageStore.useState((s) => s.selectedId === id, [id]);
  console.log("Render ListItem", item);

  const selectItem = () =>
    pageStore.update((s) => {
      s.selectedId = id;
    });

  return (
    <div
      onClick={selectItem}
      style={{
        color: selected ? "red" : "black",
        cursor: "pointer"
      }}
    >
      {item.name}
    </div>
  );
}

function List({ pageStore }) {
  const items = pageStore.useState((s) => s.items);
  console.log("Render list", items);

  return (
    <ul>
      {items.map((item) => (
        <ListItem key={item} pageStore={pageStore} id={item} />
      ))}
    </ul>
  );
}

export function Page() {
  const [pageStore] = useState(() => createPageStore());
  console.log("Page render");

  return (
    <div>
      <List pageStore={pageStore} />
    </div>
  );
}

export default function App() {
  return <Page />;
}

In this example clicking on an item will change the selectedItem and will only re-render 2 list items at max.

In this example: const [pageStore] = useState(() => createPageStore()); seems like a hack.

Mobx has concept of useLocalStore(). Is there an equivalent for pullstate? I couldn't find one. Another option could be to add "Guide" section in docs which includes some of these examples.

(In real life items and itemsById might be a different store but this gives an example of where a page may contain few state such as selectedItem)

[Question] Proper method to read from multiple stores outside of a function component?

Hey, loving the simplicity that pullstate is giving me for building a reactive ui with a global store. There are a few pieces to this question I could not find in documentation or existing issues and I suspect what I'm doing is not the right way:

I have a chart with some data. A store is populated with some default data for a default range of time. A user can change this time range which modifies a value in a store of filters. In my file that defines my stores/controllers/whatever (anything outside of the ui components), how would I go about having the main data store react to this change in filter and pull new data? They are separated because there are elements of the data that are present in other ui where filters are not.

Ideally what I'm looking for is a way to subscribe to the filter and also somehow get the value of some data to do some context specific decisions about how that filter change happens. I'm currently achieving this by subscribing to the filters and grabbing data from myStore.getRawState(). Even if this is acceptable, is there a proper/better way to do this? I suspect if I combined my stores I could grab the data I needed from the 'allstate' parameter, but I thought I'd ask now for future situations where having multiple stores merge into 1 isn't feasible.

Selecting multiple sub-states with `<InjectStore />`

I'm converting a react-copy-write over to pullstate and it's going nicely.

I'm not using functional components everywhere so I can't use hooks everywhere I'd like yet. One place where it's not so nice is selecting multiple sub-states with <InjectStore />.

An example:

<InjectStore on={state => state.previewScreenshotOS}>
  {os => (
    <InjectStore on={state => state.apps[state.selectedApp]}>
      {(app: IApp) => (
        // ...
      )}
    </InjectStore>
  )}
</InjectStore>

That is with two sub states, you can imagine that it gets a bit unwieldy with 3 or 4. With react-copy-write I was able to do the following:

<Consumer
  select={[
    state => state.previewScreenshotOS,
    state => state.apps[state.selectedApp]
  ]}
>
  {(os, app) => (
    // ...
  )}
</Consumer>

I guess a possible workaround would be to not select sub-states in <InjectStore /> and just select the entire store.

<InjectStore>
  {state => {
    const {os, app: IApp} = state;
    return (
      // ...
    )
  }}
</InjectStore>

What do you think?

Reference section for docs?

Hi, I'm trying out pullstate and noticed the docs are geared towards examples and how-tos but don't have any reference section listing types / methods / properties. Is there anywhere I can find that or any plan to create it? Thanks.

Accessing a store without a hook?

I have a function that does some updating, but I want it to be able to read the state at the beginning before it decides what to change

The function is outside a React component, so no hook option ... can the state be directly accessed any other way? (was using hookstate for a while, where the hook is just a thin wrapper)

Persist state in React Native app

Hello!

This project looks amazing! Just what I was looking for.

Do you think it would work without issues in a React Native app? And also, what would be the simplest way to persist changes to the store using AsyncStorage? (it's like LocalStorage but for React Native apps)

Any approach to computed getters?

Is there a way to define a computed getter that doesn't recalculate unless any of its dependent fields mutated?

class Theme {
  [immerable] = true;
  theme = {
    isDark: true
  };
  timesThemeChanged = 0;
  get setOppositeThemeMode() {
      console.log("should only be called once, until theme.isDark changes") 
      !this.theme.isDark;
  }
  setOppositeThemeMode() {
    this.timesThemeChanged += 1;
    this.theme.isDark = !this.theme.isDark;
  }
}

Sandbox
When refreshing the app by mutating setter, it seems to only call the getter once and reuse its value in both components, which is good.
But when editing by calling setOppositeThemeMode() it will trigger setOppositeThemeMode twice.

Could be related to batch updates, but not 100%.

Mixed CJS require and ES imports causing issues

I've been using this library for a while now, it seems really nice! ๐Ÿ‘

However, there's this one thing I noticed when trying to get this to work on a web app built with Rollup (previous project was with Webpack)

Mixed usage of CommonJS require and ES imports seems to be causing a havoc when trying to consume the library, Immer does not seem to be imported into the bundle at all, the require call is kept intact.

This is from what I can assume by looking at the bundled code and messing around with Pullstate itself, if this issue actually comes from somewhere else like in my Rollup configuration, please let me know! ๐Ÿ˜„

Retaining types when using wrappers around pullstate

Just a usage question since I'm using Typescript for the first time. Feel free to close this and not answer if you're busy. :)

I have only one store in my app so I want to wrap the existing functions in helper functions that provide the store parameter.

So, in JS it would be this:

import {
  Store,
  useStoreState,
  InjectStoreState
} from 'pullstate';

export const mainStore = new Store(state);

export const useStore = (substate) => useStoreState(mainStore, substate);
export const update = (updateFn) => mainStore.update(updateFn);
export const InjectStore = ({ on, children }) => (
  <InjectStoreState store={mainStore} on={on}>
    {injectedState => children(injectedState)}
  </InjectStoreState>
);

What is the best way to provide types for these functions that rely on the existing type annotations that you have provided in pullstate? I presume I need to be looking into generics but I'm finding it tough to get my head around.

Do state updates need to be by assignment?

That is to say, is this okay? Are there issues with this?

store.update(s => s.someArray.push(newValue);

As opposed to....

store.update(s => { s.someArray = [...s.someArray, newValue]; });

Export `PullstateInstance`?

Is it possible to use PullstateInstance as a type? I am making a function that gets passed all my stores, so I want to say:

type AllStores = PullstateInstance<{
    PlaceShareLinksStore: Store<IPlaceShareLinksStore>
}>

export type Hook = (
    toRoute: IRoute,
    toState: TRouteState,
    instance: AllStores,
) => Promise<void>

so then I can use this type for a function, like 'onEnter' where onEnter: Hook and is used like so:

        onEnter: async (_, state, Store) => {
            const { data } = await fetchPlacesFromShareId(state.id)
            await Store.stores.PlaceShareLinksStore.update((s: any) => {
                s.data = data
            })
        },

I am still learning Typescript so maybe there is a better way to handle this, although this is the only way I can see making sense so far!

What is action? Docs problem

Hi!

I'm reading docs now and I don't feel confident because the documentaion lacks important details and vague to some extent.

The first chapters are ok, except for Server Rendering which I just skipped - too many details I don't really need for now. There are also many minor issues like missed imports however.

Then comes Async Actions chapter. Without any explanations of what Sync Actions are it starts discussing Async Actions.

Url: https://lostpebble.github.io/pullstate/docs/async-actions-creating

Section: The action itself
useStoreState is not used, should be removed from the imports.

Section: Update our state stores with async actions
Here the imports section is just missed.

Url: https://lostpebble.github.io/pullstate/docs/async-action-use

Section: Watch an Async Action (React hook)

const [started, finished, result, updating] = GetUserAction.useWatch({ userId }, options);

What is GetUserAction? Is it a part of Pullstate API which you forgot to mention? The imports are missed too so it becomes unclear.

Section: (React Suspense) Read an Async Action

const posts = getPostsAction.read({ userId: "lostpebble" });

Now I really doubt GetUserAction was a Pullstate API thing. So probably both GetUserAction and getPostsAction are user-defined ones. Then how and where they are defined? Why they are object, how do they acquire those hooks?

  <Suspense fallback={<div>Loading Posts....</div>}>
    <RenderPosts/>
  </Suspense>

What is Suspense. What is fallback and why are they here?

Url: https://lostpebble.github.io/pullstate/docs/async-post-action-hook

section: DO NOT DO THIS!

const searchPicturesForTag = PullstateCore....

What is PullstateCore and why is it called like that? In the previous examples it was just Pullstate I believe.

And I haven't finished reading yet...

P.S. Please don't say that Pull Requests are welcome, because first - I know they are, and second - I'm not qualified enough to produce some nice and shiny PRs, I'm just a user trying to read some docs.

Redux Devtools integeration

I really like your library. You are doing a really good job! Thank you, Do you plan to support Redux Devtools. I know a similar state management package does this.

Redux Devtools would be great because debugging is difficult in my Google App Script project and a devtool like Redux can be easily setup for testing in react production environment .

How to use useWatch when args are changing

I had an issue that useWatch depends on args.
I want to use useWatch with some args, that will change depending on user's actions.
How should I specify useWatch to be able to track finished?

Optimistic UI with async?

Hi, I was wondering what is the best way to implement optimistic UI async actions. E.g. when the action is initiated, UI updates immediately simulating a success and optionally reverts back in case the action in fact ends up with an error.

What is the suggested way to implement this? Looking at the postActionHook, it's only called once action finishes. For optimistic UI, we need something to be called as soon as the action is started.

Thanks for this neat little library.

[Question] How to pass arguments to multiple updaters?

Hey, in the Docs you pass in an array of multiple updaters like:

CountStore.update([incrementCount, decrementCount]);

I have a function:
const incrementCount = (s) => { s.count += 1 }

and i want to use an additional parameter 'value':
const incrementCount = (s, value) => { s.count += value }

How do i pass the value as an argument in the array?

Writing CountStore.update([incrementCount(20), decrementCount]); does not work. What am i doing wrong?

Thanks!

Docs: What's the type of `type`?

Hi, first for all thanks for making this fantastic state manager.
I want to make a centralized state selectors.
This example below I saw in the doc has bring me questions.

const MyComponent = ({ type }) => {
  const data = useStoreState(MyStore, (s) => s[type], [type]);

  // OR

  const data = MyStore.useState((s) => s[type], [type]);

  // do stuff with data
}

What is type exactly? I don't have { type } prop in my components, where do this come from?
What's the type of type? It's looks like a String to me as it's using for getting sub-states from states.
Then it looks like a dependency to me.
What is it?

Use Draft<S> from immer in TUpdateFunction

This is a great small library I just found, good work : ) There's a minor issue with TypeScript that would be great if it could be changed:

interface S {
  readonly items: readonly string[];
}

const store = new Store<S>({items: []});

// this gives an error "splice does not exist on type ..."
store.update(draft => { draft.items.splice(1, 1) });

// with immer it works
declare const s: S;  
produce(s, draft => { draft.items.splice(1, 1) });

This doesn't transpile as items is a readonly array. immer has a Draft type for this case - would be great if it could be used in pullstate:

https://github.com/lostpebble/pullstate/blob/master/src/Store.ts#L26 - https://github.com/lostpebble/pullstate/blob/master/src/Store.ts#L28

import { Draft } from "immer";

export type TUpdateFunction<S> = (draft: Draft<S>, original: S) => void;
type TPathReactionFunction<S> = (paths: TAllPathsParameter<S>, draft: Draft<S>, original: S) => void;
type TReactionFunction<S, T> = (watched: T, draft: Draft<S>, original: S, previousWatched: T) => void;

Pullstate detected an infinite loop caused by cacheBreakHook()

Hello,

I am currently getting the following error:

index.js:1 [1-{id(1)}] Pullstate detected an infinite loop caused by cacheBreakHook()
returning true too often (breaking cache as soon as your action is resolving - hence
causing beckoned actions to run the action again) in one of your AsyncActions - Pullstate prevented
further looping. Fix in your cacheBreakHook() is needed.

With the following async action:

const getAppByID = createAsyncAction(
	async ({ id }: { id: number }) => {
		const { body, error } = await api.get('/app/' + id)
		if (error) {
			return errorResult([], '', error)
		}

		return successResult(body!)
	},
	{
		postActionHook: ({ result }) => {
			if (!result.error) {
				_upsert(result.payload)
			}
		},
		cacheBreakHook: () => true
	}
)

I would like the action to always be run (never cache a response). Any ideas what I may be doing wrong?

Thanks!

Single store vs. multiple stores

Hi and thanks for this solution! You are my hero! I wish I could star this even more!

After reading the docs I wasn't sure how to structure my stores: single deeply nested store vs. multiple shallow stores. Are there any drawbacks to having multiple stores? (I guess you can't update them in one step, but is that an issue?)

Can't perform a React state update on an unmounted component. When switching view with react route

I'm trying to use PullState in an app with a few react routes and get the error " Can't perform a React state update on an unmounted component." when I use some kind of callback to update the store. Small example inlined. Switch view and press the Test 1, 2 or 3 link. In the console you get (Can't perform a React state update on an unmounted component.) Seems to be "setUpdateTrigger((val) => val + 1);" in useStoreState that triggers the warning. Are we initializing the store the wrong way or should this work?

setUpdateTrigger((val) => val + 1);

store.js

import { Store } from 'pullstate';

export const UIStore = new Store({
  clickTimes: 0
});

Quick example, based on create react app and added react router

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import { useStoreState } from 'pullstate';
import { UIStore } from './store';

export default function BasicExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
        </ul>

        <hr />

        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// You can think of these components as "pages"
// in your app.

function Home() {
    const stateNR = useStoreState(UIStore, (s) => s.clickTimes);
  const updateNumber = () =>  {
    UIStore.update((s) => {
      // eslint-disable-next-line no-param-reassign
      s.clickTimes = s.clickTimes +1
    });
  }
  return (
    <div>
      <h2>Home</h2>
      <a href="#" onClick={(e) => { e.preventDefault(); updateNumber()}}>Test 1</a>
      <br />
      {stateNR}
    </div>
  );
}

function About() {
  const stateNR = useStoreState(UIStore, (s) => s.clickTimes);
  const updateNumber2 = () =>  {
    UIStore.update((s) => {
      // eslint-disable-next-line no-param-reassign
      s.clickTimes = s.clickTimes +1
    });
  }
  return (
    <div>
      <h2>About</h2>
      <a href="#" onClick={(e) => { e.preventDefault(); updateNumber2()}}>Test 2</a>
      <br />
      {stateNR}
    </div>
  );
}

function Dashboard() {
  const stateNR = useStoreState(UIStore, (s) => s.clickTimes);
  const updateNumber3 = () =>  {
    UIStore.update((s) => {
      // eslint-disable-next-line no-param-reassign
      s.clickTimes = s.clickTimes +1
    });
  }
  return (
    <div>
      <h2>Dashboard</h2>
      <a href="#" onClick={(e) => { e.preventDefault(); updateNumber3()}}>Test 3</a>
      <br />
      {stateNR}
    </div>
  );
}

Pullstate crash when upating object in array within async action

I have following async action that should update one item's content in the postActionHook (I've left out the HTTP part for better clarity, findIndex comes from lodash and it seems to work fine - returns right index):

/**
 * Update content of an announcement.
 */
export const updateAnnouncementContent = createAsyncAction(
  /**
   *
   * @param {CF2021.Announcement} item
   */
  async (item, newContent) => {
    return successResult({ item, newContent });
  },
  {
    postActionHook: ({ result }) => {
      AnnouncementStore.update((state) => {
        const itemIdx = findIndex(state.items, { id: result.payload.item.id });
        state.items[itemIdx].content = result.payload.newContent;
      });
    },
  }
);

I'm using it in my component like this:

const confirmEdit = useCallback(
    async (newContent) => {
      await updateAnnouncementContent.run(itemToEdit, newContent);
      setItemToEdit(null);
    },
    [itemToEdit, setItemToEdit]
  );

Unfortunately, this ends up in crash:

image

I wasn't able to find much about createPullstateCore but only seems useful for updates that span multiple stores which is not the case. Also, doing other updates in similar manner like appending new items or removing items works just fine.

@lostpebble Can you please shed some light on this?

Getting '_asyncCache' of null error when building pre-render of site

So I've been really loving Pullstate. I'm using it in a Preact CLI project, which uses react-compat. Everything has been working flawlessly in dev. When I attempt to do a build the CLI tries to pre-render pages. Everything is working except that I get a type error around '_asyncCache' from Pullstate dist file.

I wouldn't be posting but it looks more like a typescript error that could possible be accounted for? I'm not sure though. This may also just be a Preact pre-render incompatibility that is outside of the scope of Pullstate, which I would completely understand. Either way, I'm a bit stumped by the issue and was hoping for either a fix or "move along".

This is the error output:
` Build [=================== ] 95% (18.4s) emittingUnable to read file: /Users/robert/viewer/frontend/app/src/node_modules/pullstate/dist/index.es.js

TypeError: Cannot read property '_asyncCache' of null
method: null`

Happy to provide any other info. Thanks in advance!

run() with respectCache returns null if running twice

If I execute run() with { respectCache: true } and the same key twice - the second instance before the first finishes - the second instance returns null immediately. It's easy to see why and I would be glad to offer a PR to solve it, but I'm not 100% certain what the intended behavior would even be. ;-) So let's clarify:

Most cases are pretty clear:

  1. run() without respectCache => run individually, easy
  2. run() with respectCache, but there is already something actually cached (no matter whether another call runs, it makes sense to return cache right away, since we stated we don't need most up-to-date data) =>return the cached value, again easy
  3. run() with respectCache, while another instance is running (either because cache was empty to begin with or it was executed without respectCache) => Now it gets interesting. I personally would consider the cache still empty at this point (but pullstate currently treats it as a cached error state). Now we could start another instance or - my preference - wait for the already running instance to finish and use its result.

I don't know how exactly this api was intended, so your understanding might differ. In case you agree I can make a PR.

TypeScript error on pullstate 1.11.3

I'm getting the following TypeScript error when trying to compile a project:

ERROR in C:/.../node_modules/pullstate/dist/PullstateCore.d.ts(27,44):
TS2344: Type 'PullstateSingleton<S>' does not satisfy the constraint 'PullstateSingleton<IPullstateAllStores>'.
ERROR in C:/.../node_modules/pullstate/dist/PullstateCore.d.ts(27,92):
TS2344: Type 'PullstateSingleton<S>' does not satisfy the constraint 'PullstateSingleton<IPullstateAllStores>'.
ERROR in C:/.../node_modules/pullstate/dist/PullstateCore.d.ts(28,41):
TS2344: Type 'PullstateSingleton<S>' does not satisfy the constraint 'PullstateSingleton<IPullstateAllStores>'.

does useMemo improve perf?

Is there any difference between the following code when passing getSubState?

wihout useMemo().

const value = MyStore.useState(toFormatStr('%'));

vs

with useMemo()

const value = MyStore.useState(useMemo(() => toFormatStr('%'), []));
Here is the full code
const MyStore = new Store({
  value: 10
});

const toFormatStr = (value) => {
  console.log('outer');
  return s => {
    console.log('inner');
    return s.value + ' ' + value;
  }
}

export default function App() {
  const value = MyStore.useState(toFormatStr('%'));
  // const value = MyStore.useState(useMemo(() => toFormatStr('%'), []));
  console.log(value);

  const [count, setCount] = useState(0);

  return (
    <div className="App">
      value: {value}
      <button onClick={e=>setCount(count + 1)}>Re-render</button>
    </div>
  );
}

Is useMemo() good in this case or is not worth the optimization?

Any thoughts on realtime sync backend?

Hiya, I'm really loving this lib.

I'm switching my app over to Firebase from RESTful APIs. I just wanted to ask if you had any specific thoughts on how this might work with pullstate?

Update fast-deep-equal, consider es6 variant

The package is using [email protected], and this dependency has been updated to include support for es6 structs like Map/Set.

With [email protected] and importing as const isEqual = require("fast-deep-equal/es6"); allows using Map objects inside the store. Without it, the store doesn't detect any changes.

But perhaps that breaks for older Node versions?

(btw, thanks for the prior fix ๐Ÿ‘)

No issue - just like to say thank you

Doing some OSS too: https://github.com/i18next/react-i18next (and https://github.com/i18next/i18next) i personally would like to get more positive Feedback from time to time - so i thought - i say thank you.

I currently work on creating our next product complementing our https://locize.com (https://localistars.com) and started searching an option for state management and side effects handling.

  • redux & redux saga we used in the past - it's great but somehow it feels to heavy
  • unstated - idk but guess that is a dead horse (unmaintained, unclear direction, guess waiting for fetcher API)
  • unistore - no hooks, to much work - action function for every little manipulation
  • outstated - looks promising but like you i prefer having the store not in react but separated
  • constate - same as outstated but needs even a Provider for every store

So for me there is currently no real state management option covering my own taste - only yours.

You nailed it for me - simple store with simple update statements where needed. And AsyncActions for the heavy lifting (guess fetcher API will be similar).

Not sure about what will happen when the fetcher API lands https://xebia.com/blog/the-future-of-react-is-not-another-state-manager/ best case might be an update on the AsyncActions here.

Do you plan to promote this module more? Or will it be on risk to get unmaintained (which would be ok too - not too much code, well written, so we might maintain our own simplified fork - no need for SSR).

SO --- THANK YOU!!!

React.StrictMode causes async action listeners to not be cleaned up properly

Hi, I am using a CRA application, with lazy-loading and react-router-dom.

I am using the createAsyncAction method like this:

const getAllOrders = createAsyncAction(
  async () => {
    const res = await Connections.postRequest('getAllOrders');
    const { orders = [] } = res.data || {};
    if (res.ok) {
      return successResult(orders);
    }

    return errorResult([], `Error on getting orders: ${res.errorMessage}`);
  },
  {
    cacheBreakHook: ({ result, timeCached }) => !result.error
      && timeCached + CachingDurations.ALL_ORDERS < DateHelper.getTimestamp(),
  },
);

with custom updater inside the component:

  const [finished, result, updating] = getAllOrders.useBeckon({ tag: 'allOrders' });

...

  const updateOrders = () => {
    getAllOrders.run(
      { tag: 'allOrders' },
      {
        treatAsUpdate: true,
      },
    );
  };

I want to use the cool caching functionality, but I want also to give the user ability to update by himself the data.

So when I open for first time the route and click on the updateOrders function, it works like a charm. But when I open another page and come back to this orders-route, when I click on updateOrders I get this error message:

index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

I debug a little bit and figure out, that somehow the event listeners are not really removed when my component is unmounted, that is in the function notifyListeners:

function notifyListeners(key) {
e thi    if (clientAsyncCache.listeners.hasOwnProperty(key)) {
        for (const watchId of Object.keys(clientAsyncCache.listeners[key])) {
            clientAsyncCache.listeners[key][watchId]();
        }
    }
}

Screenshot_3

as you can see on the pic, I have 3 listeners.

I tryed to reconstruct here the same issue

https://codesandbox.io/s/pullstate-client-only-example-forked-4707o?file=/src/index.tsx

But here it works fine, and the event listeners are just cleared like they should be:

Screenshot_2

as you can see on the picture, all the old event listeners are cleared successfully and there is no such issue.

I am a little bit confused. Is there any option to clean all the previous listeners manually in the useEffect?

Thanks

Multiple async actions with one component

Hello,

I'm just getting started with React and coming from an Angular background. I have to admit that so far the learning curve on something as simple as implementing a global state object has been dumbfounding to me. In angular it's as simple as declaring and using a service.

Anyhow, I saw all the warnings about Redux and when I found this project it seemed much simpler and easier to understand. I already implemented an async action that pulls data from Google Firestore and updates my store state and displays it in a component. Great.

My first question is, what if I want to call multiple async actions in one component? I like how Apollo client returns objects rather than arrays so you can rename during destructuring. Is there a way to do this with pullstate?

For example:

const [ finished, result ] = getCart.useBeckon({});
const [ finished, result ] = getUser.useBeckon({foo:'bar'});

wouldn't work, but this would:

const {finished:cartFinished, result:cartResult} = getCart.useBeckon({});
const {finished:userFinished, result:userResult} = getUser.useBeckon({foo:'bar'});

One more question. If I wanted to keep Firestore in sync with the pullstate store, what's the recommended way of going about that? For example, any time there is an update to the pullstate store, I want it to do the same update on Firestore.

[Question] Update store not working in async function?

Sorry if this is not the right place to post this, but I've been trying to implement Pullstate into my RN application in order to reduce the amount of fetch request done throughout the app. For now, I'm fetching some basic data like the user's profile and saving it into the store.

In a "View Profile" screen I'm updating the user's profile photo with expo's image picker and it seems the property is being mutated since the new profile image is rendered as expected (there's a header component which render the profile image too) but when throwing a put request to update the profile, it seems the state before the update to the store was done, is being sent (So the original data which was fetched).

I noticed that after doing the same operation (picking a new profile image) does indeed send the previous intended data too.

  // TEST FOR GLOBAL STATE
  const userProfile = UIProfile.useState();

 const updateProfile = async () => {
  // GET THE AUTH TOKEN
  const token = await SecureStore.getItemAsync(SID_KEY);
  if (userProfile) {
    // DESTRUCTURE NEEDED DATA
    const { foto_de_perfil: profileImage, tags } = userProfile;
    // PUT REQUEST
    await onUpdate(
      { authToken: token as string },
      {
        profileImage,
        tags,
      }
    );
  }
};

const updateProfileImage = async (pickerResult: string) => {
  // UPDATE DOES NOT OCCUR ON STORE WITH THE PICKER RESULT URI
  UIProfile.update((p) => {
    if (p) {
      p.foto_de_perfil = pickerResult as string;
    }
  });

  await updateProfile();
};

// Change profile image
const openImagePickerAsync = async () => {
  const permissionResult = await ImagePicker.requestCameraRollPermissionsAsync();

  if (permissionResult.granted === false) {
    alert("Permission to access camera roll is required!");
    return;
  }

  const pickerResult = await ImagePicker.launchImageLibraryAsync({
    allowsEditing: true,
  });

  if (pickerResult.cancelled === true) {
    return;
  }
  // UPDATE THE STORE STATE
  await updateProfileImage(pickerResult.uri);
};

Update store outside of immer?

I have a unique use case where I'm updating a deep tree (imagine a DOM-like representation). For this field in my store, it's a nightmare to update and patch it using immer.

Is it possible to update the store directly without using update and the Proxy system immer uses?

I would probably then update a flag in the store that would be essentially a mock for this field to trigger re-rendering.

React Native Debugger integration

Hi was, trying to integrate with RND however I cannot get it working. I cant see anything in the Redux tab.

Here's an example of what I'm doing:

export interface VideosState {
  [videoId: string]: {
    fullscreen: boolean;
    isPlaying: boolean;
    currentTime: number;
    duration: number;
    showControls: boolean;
  };
}

export const videosStore = new Store<VideosState>({});

registerInDevtools({ videosStore });

Could anyone please confirm if it work out of the box with RND?

Warning: Can't perform a React state update on an unmounted component.

I'm getting a few warnings of:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Possibly this is related to the dynamic getSubState changes from the prior issue?

Altering this line: https://github.com/lostpebble/pullstate/blob/master/src/useStoreStateOpt.ts#L67 to return the removeListener thing as a callback instead of executing it on-mount seems to resolve it, but I have no idea if that's a valid solution.

High level brainstorming on the future of State Management

Why do we tightly couple state management with aesthetics?

React used to prone a separation between "smart" and "dumb" components. How about going one step further and implementing state management entirely independently of the view layer?

State management is business logic and is almost always entirely independent to aesthetics, CSS and HTML/DOM.

Why not doing state management entirely independently of React/Vue/Browser/iOS/Android/...?

User authentication example

Could you please provide some example of an app with user authentication?

In our apps we use redux for state management, and sagas - for async data exchange with the remote API.

Specifically, we track the following activity (actions):

  • USER_LOGIN_REQUEST
  • USER_LOGIN_PENDING
  • USER_LOGIN_SUCCESS
  • USER_LOGIN_FAILURE

Login user saga looks like this:

function* loginUserSaga({ credentials }) {
  try {
    yield put({
      type: USER_LOGIN_PENDING
    });
    const {user, timeout} = yield race({
      user: call(api.user.login, credentials),
      timeout: delay(LOGIN_TIMEOUT_SEC * 1000),
    });
    if (timeout) {
      throw new Error("Login timeout, check your network connection.");
    }
    else {
      localStorage.user = JSON.stringify(user);
      yield put({
        type: USER_LOGIN_SUCCESS,
        user
      });
    }
  }
  catch (e) {
    // noinspection JSCheckFunctionSignatures
    yield call(throwError, USER_LOGIN_FAILURE, 'Error logging in', e);
  }
}

In the real app we also track more states but the above ones are the most interesting.

I wonder how to implenent similar functionality using pullstate?

[Question] How to do async actions the Pullstate Wayโ„ข

Hi there!

I've been looking for some time for a decent alternative and less convoluted than Redux and I was ecstatic when I saw your module. Looks really nice!

I went through the docs and tried implementing it but felt a tad odd. I think my case is really straightforward and realized some people could run into this same pitfall or wonder so it could do for good examples that could enhance the docs/code.

Here's an "entity" which is a "page":

import { createAsyncAction, errorResult, successResult } from 'pullstate';
import ItemsStore from '../stores/items-store';

export function getPages() {
  return fetch(`${process.env.SERVER}/api/pages/pages?token=${process.env.TOKEN}`)
    .then(response => response.json())
    .then(json => json.map((name, index) => ({ index, name })));
}

export default createAsyncAction(
  async () => {
    const result = await getPages();

    if (result.length > 0) {
      return successResult(result);
    }

    return errorResult([], `Couldn't get pages: ${result.errorMessage}`);
  },
  {
    postActionHook: ({ result, stores }) => {
      if (!result.error) {
        ItemsStore.update(s => {
          s.pages = result.payload;
        });
      }
    },
    cacheBreakHook: ({ result, timeCached }) => {
      const CACHE_TIME = 60 * 60 * 1000; // 1 hour in milliseconds
      return timeCached + CACHE_TIME < Date.now();
    }
  },
);

Then on the view, I want to render this list of pages:

export function HomePage() {
  const [finished, result] = usePages.useBeckon();

  if (!finished) {
    return (
      <div>
        Loading...
      </div>
    );
  }

  if (result.error) {
    return <div>{result.message}</div>;
  }

  return (
    <div>
      <ul>
        {result.payload.map(page => (<li key={page.index}>{page}</li>))}
      </ul>
    </div>
  );
}

However, it feels a bit convoluted and the result.payload feels kind of dirty to me which is what got me thinking if we were doing it right.

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.