Giter Site home page Giter Site logo

atlassian / react-sweet-state Goto Github PK

View Code? Open in Web Editor NEW
862.0 71.0 55.0 2.37 MB

Shared state management solution for React

Home Page: https://atlassian.github.io/react-sweet-state/

License: MIT License

JavaScript 90.90% TypeScript 9.10%
reactjs state-management redux context-api

react-sweet-state's Introduction

react-sweet-state logo

react-sweet-state

The good parts of Redux and React Context in a flexible, scalable and easy to use state management solution

Philosophy

sweet-state is heavily inspired by Redux mixed with Context API concepts. It has render-prop components or hooks, connected to Store instances (defined as actions and initial state), receiving the Store state (or part of it) and the actions as a result.

Each Hook, or Subscriber, is responsible to get the instantiated Store (creating a new one with initialState if necessary), allowing sharing state across your project extremely easy.

Similar to Redux thunks, actions receive a set of arguments to get and mutate the state. The default setState implementation is similar to React setState, accepting an object that will be shallow merged with the current state. However, you are free to replace the built-in setState logic with a custom mutator implementation, like immer for instance.

Basic usage

npm i react-sweet-state
# or
yarn add react-sweet-state

Creating and consuming stores

import { createStore, createHook } from 'react-sweet-state';

const Store = createStore({
  // value of the store on initialisation
  initialState: {
    count: 0,
  },
  // actions that trigger store mutation
  actions: {
    increment:
      () =>
      ({ setState, getState }) => {
        // mutate state synchronously
        setState({
          count: getState().count + 1,
        });
      },
  },
  // optional, unique, mostly used for easy debugging
  name: 'counter',
});

const useCounter = createHook(Store);
// app.js
import { useCounter } from './components/counter';

const CounterApp = () => {
  const [state, actions] = useCounter();
  return (
    <div>
      <h1>My counter</h1>
      {state.count}
      <button onClick={actions.increment}>+</button>
    </div>
  );
};

Documentation

Check the docs website or the docs folder.

Examples

See sweet-state in action: run npm run start and then go and check each folder:

  • Basic example with Flow typing http://localhost:8080/basic-flow/
  • Advanced async example with Flow typing http://localhost:8080/advanced-flow/
  • Advanced scoped example with Flow typing http://localhost:8080/advanced-scoped-flow/

Contributing

To test your changes you can run the examples (with npm run start). Also, make sure you run npm run preversion before creating you PR so you will double check that linting, types and tests are fine.

Thanks

This library merges ideas from redux, react-redux, redux-thunk, react-copy-write, unstated, bey, react-apollo just to name a few. Moreover it has been the result of months of discussions with ferborva, pksjce, TimeRaider, dpisani, JedWatson, and other devs at Atlassian.


With ❤️ from Atlassian

react-sweet-state's People

Contributors

albertogasparin avatar andrewsokolov avatar buymeasoda avatar canrau avatar dawidjaniga avatar dependabot[bot] avatar drichard avatar fahmitech avatar ferborva avatar fkrafi avatar jackbrown avatar jelteliekens avatar jossmac avatar kiddkai avatar kyjor avatar mkman avatar monicaolejniczak avatar monners avatar mrceperka avatar mukuljainx avatar obweger avatar ptrmrrs avatar rohan-deshpande avatar sandorfr avatar sitek94 avatar thoiberg avatar timeraider avatar tvardero avatar vinitj avatar wisnie avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  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-sweet-state's Issues

react-sweet-state ie11 compatibility

Is the module should be developed in es5 module?

due to the lack of babel function, react-sweet-state does not work at ie11 in next js environment, thank you.

Question: Init state with hook

Is it possible to have an initial state set when using the hook API?

I was able to achieve it using the container like in the doc example.
I was thinking something in the line of useState.

Thanks.

Ability to do cleanup on store destroy

Currently, thecreateContainer provides an ability to do some cleanup on a per-container basis. It is also possible to create a single store instance shared among a number of containers and know when each of them is destroyed.

const Container = createContainer(Store, {
  onCleanup: () => {
    // This is run for every container that gets destroyed.
  }
});

Then when rendering we can do;

<Container scope="my-scope"> ... </..

We also know that by design RSS destroys the store when there are no more containers left in a given scope (for this case my-scope). Currently there is not an integrated way of knowing when the store is destroyed. This can become necessary for example to abort all ongoing requests.

As a solution consider;

const Container = createContainer(Store, {
  onDestroy: () => {
    // This is run when the store is destroyed.
  }
});

Or there may be alternatives where we can 'deduce' when to do the final cleanup, for example providing number of containers left as a prop to onCleanup etc.

Accessing previous props in onUpdate?

Hi team,

I occasionally find myself wanting to make more fine-grained decisions about what to do in onUpdate, requiring me to understand how exactly the props changed. However, as I only have access to the current props, I have to put the props into the state for later comparison. That seems not overly elegant to me. Would it be possible to make the previous props accessible in onUpdate?

Deprecate actions property in action thunk

Having all actions available in the action thunk is handy but it is a nightmare to type as you are required to use recursive types.
Better to stick with the same pattern as redux, where you call dispatch(action()).

  • Publish Typescript types without actions support
  • Remove actions arg from docs
  • Expose new ActionApi in Flow
  • Migrate examples to dispatch

Hook getStore does not scale as parent containers grow

Current context getStore method recursively looks for the container of the same type through parent containers.
However that process is repeated on every re-render and even if it's a quite fast operation, we could easily tweak the implementation to traverse the parent containers only once, on initial render.
image

The way it could work is that context getStore does not return directly the store state, but a method to get that store state from the container that holds it (we can assume that such container will hold the state for the entire lifetime of the hook, given adding/removing parent containers will trigger an unmount).
Then we can cache such method and call that one on every render.

const { findStoreGetter } = useContext(Context);
const { current: getStore } = useRef(findStoreGetter(Store));
const { storeState, actions } = getStore();

Logging unhandled actions

I was just wondering if there is currently a way to log actions that are called but not handled/don't exist?

Any examples with class components?

Like the title asks are there any examples using class components? All the examples are using hooks with functional components. Thank you!

Mocking store and selectors imported together.

I'm trying to write some tests for a container that uses react-sweet-state hooks to get store information and returns selector data.

I've scoured stack overflow for solutions, but the main solution I've found is giving me an error
TypeError: arr[Symbol.iterator] is not a function

Code:

stores/Global.js

export default createHook(Store);
export const useFooSelector = createHook(Store, {
  selector: fooSelector,
});
index.jsx

import useGlobalStore, { useFooSelector } from '../../stores/Global';

const App = () => {
  const [{ state }, { actions }] = useGlobalStore();
  const [{ foo }] = useFooSelector();
}
test.js

const mockUseGlobalStore = jest.fn();
const mockUseFooSelector = jest.fn();

jest.mock('../../../stores/Global', () => ({
  __esModule: true,
  default: () => mockUseGlobalStore,
  useFooSelector: () => mockUseFooSelector,
}));

test('should render', async () => {
    mockUseFooSelector.mockImplementation(() => [{ foo: 1 });
    mockUseGlobalStore.mockImplementation(() => [{ state },{ actions }]);
    render(<App />);
});

This gives me an error that arr[Symbol.iterator] is not a function. But I'm not sure how to set up the mock for this to work.

Having troubles setting everything up

Hi there,

So I've recently started working with react-sweet-state as an alternative to the Redux boilerplate horror.

This is what I've created so far:

Store:

import { createStore, createHook } from 'react-sweet-state'

const initialState = {
	startHolidayButton: true,
}

const actions = {
	setStartHolidayButton: value => ({ setState }) => {
		setState({ startHolidayButton: value })
	},
}

const ButtonsVisibleStore = createStore({
	initialState,
	actions,
	name: 'ButtonsVisibleStore',
})

export const useButtonsVisible = createHook(ButtonsVisibleStore)

App.js :

const App = () => {
	const [state, actions] = useButtonsVisible()
	return (
		<Styled>
			<MainMenu />
			<div className="l-opacity">
				<WorldMap />
			</div>

			{state.startHolidayButton && (
				<div
					className="bottom-toolbar"
					onClick={() => {
						actions.setStartHolidayButton(false)
					}}>
					<StartSelectionButton />
				</div>
			)}
		</Styled>
	)
}

I can read the state and trigger the actions, but my component is not re rendering, am I doing something wrong?

Comparison with other global state management solutions

Do you plan adding to the docs a comparison to other solutions like redux or mobx. I'd like to know how to classify this library / framework and where it shines / fails comparing to more popular solutions.

Also - if I'm coming from the redux world - what are the biggest selling points of react-sweet-state that can convert me to its biggest fan 😄

Displaying store name in redux dev tools

Is there a way of displaying the store name next to an action in redux dev tools. I can imagine prefixing the action type of having an extra meta attribute there to see which store actually handled the action.

Single/scope containers don't auto remove store when unmounted

I checked that react-sweet-state will remove the store when unmounted in case it hasn't had any subscriber. But it didn't work in my case. I debugged and found that the deleting happens before subscribers unsubscribed. Did i wrong somewhere or is this the bug?

Store instance actions typed incorrectly

export type StoreInstance<ST, AC> = {
  store: StoreState<ST>,
  actions: AC,
};

Should be

export type StoreInstance<ST, AC> = {
  store: StoreState<ST>,
  actions: BoundActions<AC>,
};

How to call an action from NextJS getInitialProps?

Do you have any idea how I can I call an action from within getInitialProps in a NextJS app. My action makes a /graphql request and then updates the state. Therefore, I need to have that state before the first server side render.

Here is a component I use in my _document.tsx render method to add the state to a script tag. I also have code to parse it and re-hydrate the store on client side. However, I currently get errors when the action is fired outside of the getInitialProps since it tries to render again since the state has chanegd.

import React, { FunctionComponent } from 'react';

import { useStore } from '../..';
import { isBrowser } from '../../../utils/env';
import { InitialStateScriptProps } from './types';

export const INITIAL_STATE_SCRIPT_NAME = '__INITIAL_STATE__';

/**
 * Get initial state from InitialStateScript
 *
 * @param name (optional) falls back to INITIAL_STATE_SCRIPT_NAME
 */
export const getInitialState = (name?: string) =>
  isBrowser() ? (window as any)[name ? name : INITIAL_STATE_SCRIPT_NAME] : undefined;

export const InitialStateScript: FunctionComponent<InitialStateScriptProps> = props => {
  const { name = INITIAL_STATE_SCRIPT_NAME } = props;
  const [state] = useStore();
  const INITIAL_STATE = JSON.stringify(state);
  return (
    <script
      /* eslint-disable react/no-danger-with-children */
      dangerouslySetInnerHTML={{ __html: `window.${name} = ${INITIAL_STATE};` }}
    />
  );
};

I'm not sure whether I should be calling my action in getInitialProps or whether I should be doing something like Apollo GraphQL where I make all my /graphql calls in my _app.tsx and then I rewind the Head and then store the result of the call in the store. It's a bit tricky to use Apollo GraphQL, NextJs and ReactSweetState together . . .

Circular error with MongoDB Stitch authenticated user

Hello all,

I'm using https://www.npmjs.com/package/mongodb-stitch-server-sdk with Typescript and I get circular error when I try to set user object to a state: client.auth.user

create.js:7 Uncaught TypeError: Converting circular structure to JSON --> starting at object with constructor 'StitchUserImpl' | property 'auth' -> object with constructor 'StitchAuthImpl' --- property 'currentUser' closes the circle at JSON.stringify (<anonymous>) at createKey (create.js:7) at Object.get key [as key] (create.js:19) at StoreRegistry.generateKey (registry.js:49) at registry.js:35 at useSweetState (hook.js:79) at LoginForm (LoginForm.tsx:16) at renderWithHooks (react-dom.development.js:14825) at mountIndeterminateComponent (react-dom.development.js:17505) at beginWork (react-dom.development.js:18629) at HTMLUnknownElement.callCallback (react-dom.development.js:188) at Object.invokeGuardedCallbackDev (react-dom.development.js:237) at invokeGuardedCallback (react-dom.development.js:292) at beginWork$1 (react-dom.development.js:23234) at performUnitOfWork (react-dom.development.js:22185) at workLoopSync (react-dom.development.js:22161) at performSyncWorkOnRoot (react-dom.development.js:21787) at scheduleUpdateOnFiber (react-dom.development.js:21219) at updateContainer (react-dom.development.js:24407) at react-dom.development.js:24792 at unbatchedUpdates (react-dom.development.js:21934) at legacyRenderSubtreeIntoContainer (react-dom.development.js:24791) at Object.hydrate (react-dom.development.js:24857) at renderReactElement (index.js:38) at doRender$ (index.js:44) at tryCatch (runtime.js:45) at Generator.invoke [as _invoke] (runtime.js:271) at Generator.prototype.<computed> [as next] (runtime.js:97) at tryCatch (runtime.js:45) at invoke (runtime.js:135) at runtime.js:170 at new Promise (<anonymous>) at callInvokeWithMethodAndArg (runtime.js:169) at AsyncIterator.enqueue [as _invoke] (runtime.js:192) at AsyncIterator.prototype.<computed> [as next] (runtime.js:97) at Object.push../node_modules/next/node_modules/regenerator-runtime/runtime.js.exports.async (runtime.js:216) at doRender (index.js:40) at render$ (index.js:25) at tryCatch (runtime.js:45) at Generator.invoke [as _invoke] (runtime.js:271) at Generator.prototype.<computed> [as next] (runtime.js:97) at tryCatch (runtime.js:45) at invoke (runtime.js:135) at runtime.js:170 at new Promise (<anonymous>) at callInvokeWithMethodAndArg (runtime.js:169) at AsyncIterator.enqueue [as _invoke] (runtime.js:192) at AsyncIterator.prototype.<computed> [as next] (runtime.js:97) at Object.push../node_modules/next/node_modules/regenerator-runtime/runtime.js.exports.async (runtime.js:216) at render (index.js:25)

Appreciate looking at this issue.

Question: Middleware with typescript.

Hello! Not sure this is the right place for these kinds of questions but was unsure where to get this information. It says in your documentation that adding middleware uses the same syntax as Redux middleware. These seems to mostly work with strict typing when using typescript, but I want to make sure I am understanding the types correctly.

In your example you have:

const logger = storeState => next => fn => {}

As the signature for the middleware. I believe that storeState is basically just the Store object we create, the default next function is the default mutator that processes the partial state, but I am confused as to what the fn parameter is. When I log it out it seems to be the new partial state after the transformation, is that correct? How are we meant to type these middlewares? when I use the react Middleware type, I don't get access to the correct fields in my store state, as there is no way to parameterize the type. Am I supposed to import redux types for this?

Any help would be greatly appreciated. Thanks!

batchUpdates does not work as expected

This was a feature we have been looking forward to! (We currently use our own not-so-good solution to make some of our state updates atomic) Yet I suspect it is not working as we expected it to behave (maybe we are expecting too much but reading the inline docs and code this is how I think it should be). Consider this simple example;

// fetch is a generic action we use to make async network calls, it does a setState and updates a value on store
dispatch(fetch)
  // then the consumer derives a new value and stores it
  .then((data) => setState(deriveNewData(data)))

With this, we do two independent setStates one in the generic action and one inside .then and thus we will have two independent updates; one with fetch status (e.g isCompleted: true) and one with the derived data. We are currently deferring the first set state to the next event loop via setTimeout and this provided a better behaviour on average. (i.e we do not set isCompleted before data is ready anymore, but now data is updated earlier. This is just a temporary hack on our side. We were planning to implement a middleware next if this was not on the library itself.

I expected defaults.batchUpdates = true; to batch these two updates, but it is not the case. The schedule method never has a QUEUE larger than 1 and I suspect this is because of Promise.resolve().then not deferring the callback to the next event loop but instead is processed on the same phase, before the consumer's then is executed. See here, chrome should behave very similar to that;

generally, when the event loop enters a given phase, it will perform any operations specific to that phase, then execute callbacks in that phase's queue until the queue has been exhausted or the maximum number of callbacks has executed.

Which implies an immediately resolved Promise's success callback is added to the current phase when the outerHandler is first executed. Even if res is called before it, the engine will register the inner function before the outer handler as it has already started executing the outerHandlerand will not defer the inner promise to the next loop.

new Promise(function outerHandler(res) {
  res();
  Promise.resolve().then(() => console.log('inner resolved'));
}).then(() => console.log('outer resolved'))

Prepare docs structure to allow multiple languages

Currently the docs are only written in English and the docs folder structure is setup for only one language. A first nice step to ease the creation of a translated version is to create a /en/ folder with the exisiting docs and update the links.

If you're not sold on the idea of denoting the main language (english) with a new route it can be left as is and have the translations placed inside subfolders. However, if we do have all languages in their own subfolder (/en/, /es/, /it/ ...) we can setup the top-most level for a page that displays links to all available language options.

Thoughts?

Problem using with useEffect

I have a really simple example that I can't seem to get working. The idea is simply to display whether the component is loading or not, with the action for the background work being dispatched in a useEffect hook. However, after the work is done and the state gets updated, the component doesn't seem to re-render, and I just can't figure out why, so I assume I must be doing something wrong. Attached is the example "sweet.zip", hopefully somebody else can quickly determine what's going on.

sweet.zip

Nested scoped containers

When global containers are nested, they share the global store instance;

<Container isGlobal>
    // The store instance here,
    <Container isGlobal>
        // is the same as this one
    </Container>
</Container>

The same is not true for nested scoped containers and I'd expect them to work similarly. e.g;

<Container scope="my-scope">
    // The store instance here,
    <Container scope="my-scope">
        // is NOT the same as this one
        // This should be consistent with global store behaviour
    </Container>
</Container>

Clarification on usage for multiple independent state items

Hi there,

We just started using this a couple of weeks ago in a React Native project and it's doing the job for us well.

I wanted to clarify what the intended usage is for cases with multiple separate independent pieces of state. All the documentation and examples I've seen (and I apologise if I've actually missed this somewhere) just cater of one piece of state, eg. "counter".

Should we be using a separate hook and store for each thing we want to store? Or should we just have one hook and one store which contains a state object with all our different items of state in it - and I guess in this case you would have multiple actions to set individual specific items in the state?

Or does it not really matter either way?

Thanks
Glenn

Cross-store selectors and computed state

If I decide to normalize the state and decompose my state management logic into multiple files will I be able to get the computed state efficiently without writing dirty hacks to query state from multiple stores using reselect?

How can I emit action from other store?

I have a case where I have an async action in which I want to emit action from different store. Is that possible? Or should I combine this kind of logic on the component level using hooks?

How to test if a call to an action forces component re-render.

There isn't much documentation on how to test a particular action call and see how it affects the way the component renders.

Example:

Below I have a Fizz.tsx component that calls an action inside useEffect:

export const Fizz = () => {
  const [state, actions] = useBuzzStore();

  useEffect(() => {
    actions.changeFoo(100);
  }, [actions]);

  return (
    <div>
      {state.foo > 0 ? <h2>Foo is {state.foo}</h2> : null}
    </div>
  );
};

This is my store:

import { createStore, createHook, StoreActionApi } from "react-sweet-state";

interface State {
  foo: number;
}

type StoreApi = StoreActionApi<State>;

export const initialState: State = {
  foo: 0
};

export const actions = {
  changeFoo: (num: number) => ({ getState, setState }: StoreApi) => {
    setState({
      foo: isNaN(+num) ? 0 : num
    });
  }
};

type Actions = typeof actions;

const Store = createStore<State, Actions>({
  initialState,
  actions,
  name: "buzzStore"
});

export const useBuzzStore = createHook(Store);

Basically if state.foo is greater than zero, the component renders an h2 with state.foo's value. If it's zero it renders nothing.

How would test this within jest and enzyme in a way where I can invoke the action from within the test and then write an assertion like:

describe("the fizz component", () => {
  let wrapper: ShallowWrapper;

  beforeEach(() => {
    wrapper = shallow(<Fizz />);
  });

  afterEach(() => {
    wrapper.unmount();
  });

  it("should not render an h2", () => {
    // NO CALL to action.changeFoo();

    expect(wrapper.find("h2").exists()).toBeFalsy();

    // mock the action for action.changeFoo(mockValue)
  });

  it("should render an h2", () => {
    const numberForFoo = 100;

    // CALL action.changeFoo(numberForFoo)

    expect(wrapper.find("h2").text()).toContain("Foo is " + numberForFoo);
  });
});

Link to the codesandbox: https://codesandbox.io/s/zen-greider-hohdt

Testing composed actions

I am a little confused with the example posted in the docs here about testing for composed actions.

If I wanted to assert that clickAnalytics is being dispatched inside of clickManager how would I go about that? It seems if I want to check if the mocked dispatch fn is being called I would only be able to check if there was some call to dispatch using expect.any(Function) since it would do a comparison on an anonymous fn but I wouldn't be able to assert if it was exactly clickAnalytics that was being called.

Is there any other way that I'm missing? Or does it suffice to make an assertion that somewhere dispatch is being called with some function?

`createStore` type definition should only allow object state

The current TypeScript definitions seem to allow me to create a store with a non-object state type, for example:

type State = string | undefined;
const initialState = undefined;

// ...define actions, etc...

const Store = createStore<State, Actions>({
  initialState,
  actions
});

This behaves weirdly at runtime when actions attempt to update the state (e.g. I end up with state as {0: "1"} state if I attempt to set the state to "1").

It is mentioned in the docs that the state has to be an object, but ideally the type definitions would enforce this as well.

May also apply to the Flow types - haven't checked them.

Accept symbols for scope

Currently a container accepts a scope prop and its store instance is shared with other containers having the same scope. This may rarely cause collisions in complicated scenarios. Considering one would generally want to have "unique" scopes for different sections of a react application, I think we should accept symbols to make scope management a little bit more robust when we want to.
This is not something of high priority though. Would be a nice addition.

Protect agains re-render loops if actions set state equal

It might happen that a simple action calls setState generating the same shallow equal store state again.
We want to avoid unnecessary re-renders (and potentially loops) for this scenario, so we always shallow check before re-rendering

Add support for per-scope selector instantiation

When creating a hook, it is possible to provide a selector. Consider this contrived example which will just select a property on the store;

const useValue1 = createHook(Store, {
  selector: (state) => state.value1 + state.value2,
});

When this hook is used the selector will get executed with the up to date state and return the required value. In the docs, it is also mentioned that we can use memoized selectors (e.g reselect). The following example will not execute the (value1, value2) => value1 + value2 selector unless the output of value1Selector or value2Selector (which might also be memoized) changes;

const useValue1 = createHook(Store, {
  selector: createSelector(
    [value1Selector, value2Selector],
    (value1, value2) => value1 + value2
  ),
});

The problem with this is that we are "creating" a selector once when we are creating the hook. If we use this selector with multiple instances of the store (say different containers) the memoization will loose its effectiveness as the single level of memoization will think the values has changed whenever a new mutation (with a potentially different store state) is initiated one after another. (Also see this)

To be able to utilize selectors at their best, we need a way to instantiate selectors together with store instances and this requires accepting a selector "creator" that will get executed once for each scope.

const useValue1 = createHook(Store, {
  // This function will get executed and the resulting selector is tracked internally when necessary
  selector: () => createSelector(
    [value1Selector, value2Selector],
    (value1, value2) => value1 + value2
  ),
});

It maybe ok to have another option named selectorCreator, but this may make the API a little confusing for future users. Changing the default behaviour on the other hand is probably not a good idea.

[question]: How to handle dependent state updates?

Hello,

is there any change, how to solve dependent state updates? Without passing newState as arg to particular hook?

Consider this example please:

export const useFetchDimensionsFromIndexFn = () => {
  const [availableTypes] = useDimensionTypes()
  // ...ton of other deps...
  return () => {
    // do some fetches based on `availableTypes`
  }
}
export const useUserInfoFn = () => {
  const userActions = useUserActions()[1]
  const dimensionActions = useDimensionActions()[1]
  const fetchDimensionItemsFromIndex = useFetchDimensionsFromIndexFn()

  const fn = async () => {
    try {
      const next = await userActions.fetchUserInfo()
      
      // How do I wait here? for new dimensionTypes - result of dimensionActions.setTypes
      dimensionActions.setTypes(next.dimensions.map((d) => d.type))
      // Depends on dimension.types
      // I don't want to pass it to `fetchDimensionItemsFromIndex` as arg
      // This will run with stale state!
      await fetchDimensionItemsFromIndex(0, 'union')
    } catch (e) {}
  }

  return fn
}

Workaround solution:

export const useUserInfoFn = () => {
  const userActions = useUserActions()[1]
  const dimensionActions = useDimensionActions()[1]
  const fetchDimensionItemsFromIndex = useFetchDimensionsFromIndexFn()
  const [shouldFetch, fetchItems] = React.useState(false)

  React.useEffect(() => {
    const fn = async () => {
      if (shouldFetch) {
        await fetchDimensionItemsFromIndex(0, 'union')
        fetchItems(false)
      }
    }
    fn()
  }, [shouldFetch])

  const fn = async () => {
    try {
      const next = await userActions.fetchUserInfo()
      dimensionActions.setTypes(next.dimensions.map((d) => d.type))

      fetchItems(true)
    } catch (e) {}
  }

  return fn
}

I do not like workaround solution at all. Could someone recommend me an architectural change or best practices please? 🙏

Note: I am migrating redux-logic to react-sweet-state, and some "logics" are performing getState(), which will get them fresh state (even if it was updated during execution of that "logic").

Many thanks!

Extend Store functionality

Background

As we discussed in SweetState brownbag session, I was wondering if it makes sense to add a function to extend an existing store.

The idea is that I really like sweet state that store is going to be light-weighted, but in the other side, there could be multiple stores with very similar shape. For example, we might have many different modals. We might want to have a mother store shape with mother actions. In different modals, we can extend this store.

I see benefits of extending an existing Store:

  • We can remove boiler plate code for similarly shaped stores. But carefully only when it makes sense. Dialog state is a good example.
  • We have a single test to test mother store. Child store will need to write only tests that are specific to the child store. This also removes more boiler plate of test.
  • I understand, as of today, we can reuse actions across different stores. But this might not be a good idea in terms of coupling code. An action could be doing the exactly same thing today, but we never know it might now tomorrow. Extending store solves this issue.
  • This is just extending an existing store, that does not add much complexity to the library and should be quite simple to implement.

What I imagine is something like this, but, of course, could be different.

createExtendedStore(Store, {initialState, actions, name});

initialState and actions will accept only additional ones. If an existing property is given to initialState, initial state will be overrided.

Need further discussion for items:

  1. First of all, is this going to be valuable?
  2. Do we allow action override?
  3. Maybe it is a good idea to auto name it if name is not given and mother store name exist?
  4. What are the downsides?

IMHO,

  1. I think it is going to be valuable with reasons above.
  2. I think we should allow it. That is the nature of inheriting. If this is not possible, child store might have to introduce ugly actions to change child state.
  3. I think auto naming should be done if not given, it will be very useful for debugging.
  4. This is same in OOP world, though. If you design mother store not properly, we might end up creating an ugly child stores. For example, creating a mother store and extends it, only because they share some attributes and actions, not because it makes sense semantically. Mother store should be used when it makes sense semantically. It is same as you don't create parent class only because it shares some properties in OOP. Also, Alberto pointed out that when the number of contributors are many in a codebase, many people might attempt changing the mother store, which becomes nightmare in the end. But I feel like these downsides are a generic issue of programming, not a specific issue to this feature.

What do you think?

Change in props of Container causes store to update but doesn't re render the Container itself

Is this normal behaviour?
I have a custom Container. It was created using createContainer() function. I passed in onInit and onUpdate as actions to it. They look somewhat like this:
The container is used in this way:
props = { numStars:4, reviews: [<some-elements-here>] }
<MyContainer
  scope={scope}
  displayName={scope}
  {...props}
  state={state}
>
{getMyComponent()}
</MyContainer>

The container is created using these actions:
onInit: () => ({ setState }, { state }) => {
  setState(state);
},
onUpdate: () => ({ setState }, { state, numStars, reviews }) => {
  updateStateFromKnobs(istate, { numStars, reviews }, setState);
}

State here is being given as:
{
  someOtherProps,
  product: {numStars, reviews}
}

In updateStateFromKnobs() function, I simply call this:
const product = {numStars, reviews}
const newState = {
  ...state,
  product
};
setState(newState);

Now, whenever I change the props of the Container, the store changes, I can see it in Redux Dev Tools, but the component doesn't re-render. Is this expected behaviour? How do make the component re-render? This component is inside the Container as returned by getMyComponent() function.

TypeScript support

I see the project is typed using Flow - is there a plan to write TS typings for it?

Can you tighten typescript types or add runtime development warning what is valid state in store?

I was just trying out react-sweet-state as it looks interesting and created a small to do list app to play with it.

I put an array of Todo objects in an array as the state of the store.
This produces no errors from typescript and even runs without error until you setState new values of the state.

The state of the store being an array is not supported as I later discovered in the react-sweet-state documentation under setState().

I think you might help developers by either tightening the types allowed for the state of store, or maybe adding a runtime warning in development mode from react-sweet-state.

I stripped down the app I was playing with to make a small example to demonstrate the behavior I am describing.
https://codesandbox.io/s/eloquent-hill-vh4xb?file=/src/App.tsx

If you change the text of a to do item it calls setState and that crashes this app because after set state the value of state is of the form { 0: [Todo1, Todo2] }

I quite like sweet state so far 👍
Thank you for making it available.

Thank You

I've jumped around trying different state management libraries for React and react-sweet-state is easily the best in my opinion. I hooked this package up in one of my projects and just works as intended! I love it, thank you so much for building this amazing library ❤️ !

How to display components that uses store created using RSS (react-sweet-state) in Storybook ?

The more idiomatic way is presentational/container components but wondering if there is any other way.

Take for example Todo list .. it can be in different states and I would like to have stories for each of them like

yet to be done
partially completed
all completed
Editing stating
In the case of redux, I can wrap it with a provider and give an appropriate initial state .. so automatically I can force it to be in my desired state.

Coming to RSS .. I like the simplicity but how do I give different initial states for each story?

Actions need to be bound recursively

Seems like actions are only being bound one level deep. There needs to be a way to recursively bind them

eg.,

Works

import {
  createStore,
  createHook,
  defaultRegistry,
  createContainer,
  createSubscriber
} from "react-sweet-state";
import {
  getRouteContext,
  requestRouteResources,
  resolveAllResourceRequests
} from "../router/utils";

const onInit = () => async ({ getState, setState, actions }, initialState) => {
  const { location, routes, isServer } = initialState;
  const context = await actions.requestRouteResources({
    location,
    routes,
    isServer
  });

  setState({ ...initialState, ...context });
  actions.listenToBrowserHistory();
};

const RouterStore = createStore({
  initialState: {
    history: null,
    location: {
      pathname: null,
      search: null,
      hash: null
    },
    routes: null,
    route: null,
    match: null,
    action: null
  },
  actions: {
    onInit,
    requestRouteResources: ({
      location,
      routes,
      isServer = false
    }) => async () => {
      const context = getRouteContext(location, routes);
      const { route } = context;

      if (!route) {
        return null;
      }

      requestRouteResources(route);

      if (isServer) {
        await resolveAllResourceRequests();
      }

      return context;
    },

    listenToBrowserHistory: () => ({ getState, setState, actions }) => {
      const { history, routes } = getState();

      history.listen(async (location, action) => {
        const context = await actions.requestRouteResources({
          location,
          routes
        });

        context && this.setState(context);
      });
    }
  }
});

export const RouterContainer = createContainer(RouterStore, {
  onInit
});

export const RouteSubscriber = createSubscriber(RouterStore);

Breaks

import {
  createStore,
  createHook,
  defaultRegistry,
  createContainer,
  createSubscriber
} from "react-sweet-state";
import {
  getRouteContext,
  requestRouteResources,
  resolveAllResourceRequests
} from "../router/utils";

const RouterStore = createStore({
  initialState: {
    history: null,
    location: {
      pathname: null,
      search: null,
      hash: null
    },
    routes: null,
    route: null,
    match: null,
    action: null
  },
  actions: {
    onInit: initialState => async ({ getState, setState, actions }) => {
      const { location, routes, isServer } = initialState;
      const context = await actions.requestRouteResources({
        location,
        routes,
        isServer
      })(); // not sure why I have to call twice here

      setState({ ...initialState, ...context });
      actions.listenToBrowserHistory();
    },
    requestRouteResources: ({
      location,
      routes,
      isServer = false
    }) => async () => {
      const context = getRouteContext(location, routes);
      const { route } = context;

      if (!route) {
        return null;
      }

      requestRouteResources(route);

      if (isServer) {
        await resolveAllResourceRequests();
      }

      return context;
    },

    listenToBrowserHistory: () => ({ getState, setState, actions }) => {
      const { history, routes } = getState();

      history.listen(async (location, action) => {
        const context = await actions.requestRouteResources({
          location,
          routes
        });

        context && this.setState(context);
      });
    }
  }
});

export const RouterContainer = createContainer(RouterStore, {
  onInit: () => ({ actions }, props) => {
    actions.onInit(props);
  }
});

export const RouteSubscriber = createSubscriber(RouterStore);

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.