Giter VIP home page Giter VIP logo

undux's Introduction

undux

Build Status npm mit ts flow

Dead simple state management for React


📖 Official docs: https://undux.org


Install

# Using Yarn:
yarn add undux

# Or, using NPM:
npm install undux --save

Install (with RxJS v4-)

# Using Yarn:
yarn add undux@^3

# Or, using NPM:
npm install undux@^3 --save

Design Goals

  1. Complete type-safety, no exceptions
  2. Super easy to use: forget actions, reducers, dispatchers, containers, etc.
  3. Familiar abstractions: just get and set

Read more here

Use

1. Create a store

import { createConnectedStore } from 'undux'

// Create a store with an initial value.
export default createConnectedStore({
  one: 0,
  two: 0,
})

Be sure to define a key for each value in your model, even if the value is initially undefined.

2. Connect your React components

With React Hooks: useStore

import { useStore } from './MyStore'

// Re-render the component when the store updates.
function MyComponent() {
  const store = useStore()
  return (
    <>
      <NumberInput onChange={store.set('one')} value={store.get('one')} />
      <NumberInput onChange={store.set('two')} value={store.get('two')} />
      Sum: {store.get('one') + store.get('two')}
    </>
  )
}

function NumberInput({ onChange, value }) {
  return (
    <input
      onChange={(e) => onChange(parseInt(e.target.value, 10))}
      type="number"
      value={value}
    />
  )
}

export default MyComponent

Without React Hooks: withStore

import { withStore } from './MyStore'

// Re-render the component when the store updates.
function MyComponent({ store }) {
  return (
    <>
      <NumberInput onChange={store.set('one')} value={store.get('one')} />
      <NumberInput onChange={store.set('two')} value={store.get('two')} />
      Sum: {store.get('one') + store.get('two')}
    </>
  )
}

function NumberInput({ onChange, value }) {
  return (
    <input
      onChange={(e) => onChange(parseInt(e.target.value, 10))}
      type="number"
      value={value}
    />
  )
}

export default withStore(MyComponent)

3. Put your app in an Undux Container

import MyComponent from './MyComponent'
import { Container } from './MyStore'

function MyApp() {
  return (
    <Container>
      <MyComponent />
    </Container>
  )
}

export default MyApp

That's all there is to it.

Open this code in playground.

Features

Effects

Though Undux automatically re-renders your connected React components for you when the store updates, it also lets you subscribe to changes to specific fields on your store. Undux subscriptions are full Rx observables, so you have fine control over how you react to a change:

import { debounce, filter } from 'rxjs/operators'

store
  .on('today')
  .pipe(
    filter((date) => date.getTime() % 2 === 0), // Only even timestamps.
    debounce(100), // Fire at most once every 100ms.
  )
  .subscribe((date) => console.log('Date changed to', date))

You can even use Effects to trigger a change in response to an update:

store
  .on('today')
  .pipe(debounce(100))
  .subscribe(async (date) => {
    const users = await api.get({ since: date })
    store.set('users')(users)
  })

In order to keep its footprint small, Undux does not come with RxJS out of the box. However, Undux does come with a minimal implementation of parts of RxJS, which interoperates with RxJS operators. To use RxJS operators, you'll need to install RxJS first:

npm install rxjs --save

Partial application

Partially apply the set function to yield a convenient setter:

const setUsers = store.set('users')
setUsers(['amy'])
setUsers(['amy', 'bob'])

Built-in logger

Undux works out of the box with the Redux Devtools browser extension (download: Chrome, Firefox, React Native). To enable it, just wrap your store with the Redux Devtools plugin:

import { createConnectedStore, withReduxDevtools } from 'undux'

const store = createConnectedStore(initialState, withReduxDevtools)

Redux Devtools has an inspector, a time travel debugger, and jump-to-state built in. All of these features are enabled for Undux as well. It looks like this:

Alternatively, Undux has a simple, console-based debugger built in. Just create your store with withLogger higher order store, and all model updates (which key was updated, previous value, and new value) will be logged to the console.

To enable the logger, simply import withLogger and wrap your store with it:

import { createConnectedStore, withLogger } from 'undux'

let store = createConnectedStore(initialState, withLogger)

The logger will produce logs that look like this:

Effects

Undux is easy to modify with effects. Just define a function that takes a store as an argument, adding listeners along the way. For generic plugins that work across different stores, use the .onAll method to listen on all changes on a store:

// MyStore.ts (if using TypeScript)
import { Effects } from 'undux'

type State = {
  // ...
}

export type StoreEffects = Effects<State>

// MyEffects.ts
import { StoreEffects } from './MyStore'

const withLocalStorage: StoreEffects = (store) => {
  // Listen on all changes to the store.
  store
    .onAll()
    .subscribe(({ key, value, previousValue }) =>
      console.log(key, 'changed from', previousValue, 'to', value),
    )
}

Recipes

Creating a store (TypeScript)

// MyStore.ts
import { createConnectedStore, type Effects, type Store } from 'undux'

type State = {
  foo: number
  bar: string[]
}

const initialState: State = {
  foo: 12,
  bar: [],
}

export default createConnectedStore(initialState)

// If using effects..
export type StoreEffects = Effects<State>

// If using class components..
export type StoreProps = {
  store: Store<State>
}

See full example (in JavaScript, TypeScript, or Flow) here.

Function component (TypeScript)

// MyComponent.ts
import { useStore, type StoreProps } from './MyStore'

type Props = {
  foo: number
}

function MyComponent({ foo }: Props) {
  const { store } = useStore()
  return (
    <>
      Today is {store.get('today')}
      Foo is {foo}
    </>
  )
}

export default MyComponent

// App.ts
import { Container } from './MyStore'

function App() {
  return (
    <Container>
      <MyComponent foo={3} />
    </Container>
  )
}

export default App

See full example (in JavaScript, TypeScript, or Flow) here.

Class component (TypeScript)

Undux is as easy to use with class components as with function components.

// MyComponent.ts
import { withStore, type StoreProps } from './MyStore'

type Props = StoreProps & {
  foo: number
}

class MyComponent extends React.Component<Props> {
  render() {
    return (
      <>
        Today is {this.props.store.get('today')}
        Foo is {this.props.foo}
      </>
    )
  }
}

export default withStore(MyComponent)

// App.ts
import { Container } from './MyStore'

function App() {
  return (
    <Container>
      <MyComponent foo={3} />
    </Container>
  )
}

export default App

See full example (in JavaScript, TypeScript, or Flow) here.

Undux + Hot module reloading

See a full example here.

Undux + TodoMVC

See the Undux TodoMVC example here.

Design philosophy

Goal #1 is total type-safety.

Getting, setting, reading, and listening on model updates is 100% type-safe: use a key that isn't defined in your model or set a key to the wrong type, and you'll get a compile-time error. And connected components and Effects are just as type-safe.

Goal #2 is letting you write as little boilerplate as possible.

Define your model in a single place, and use it anywhere safely. No need to define tedious boilerplate for each field on your model. Container components and action creators are optional - most of the time you don't need them, and can introduce them only where needed as your application grows.

Goal #3 is familiar abstractions.

No need to learn about Actions, Reducers, or any of that. Just call get and set, and everything works just as you expect.

Tests

npm test

License

MIT

undux's People

Contributors

8th713 avatar bcherny avatar fredkschott avatar izumisy avatar jbrown215 avatar kassens 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

undux's Issues

Investigate marking a React node as a store "owner"

An issue with Flux and Redux is the inability to garbage collect stores. Today, Undux suffers from the same issue: because each store is a singleton defined at the top level in a module, there's no way to safely garbage collect it today.

This is an issue when using Undux in complex applications with many React roots (ie. ReactDOM.render is called for lots of mount points), like facebook.com. In applications like that, if a particular root makes use of a particular Undux store, it's desirable to GC the store when its corresponding React root unmounts.

One way to do that is to speciate withStore into two kinds of withStore:

  1. A withStore that indicates that the component that is wrapped is a React root. When that component unmounts, we should GC the store. Let's call it withStoreRoot.
  2. A withStore that indicates that the component this is wrapped is not a React root, and is just a regular store-consuming component. This is how withStore behaves today.

This approach would also mean forbidding declaring and using store at the top level. Instead, we'd expose a way to create a "store factory", and store could only be used in React components and Undux Plugins. This has the downside of making writing Undux Actions more painful, but could ultimately be a good way to enforce good architecture (similar to #36).

If we go with this approach, we need a way to enforce that all withStore calls for a given store are descendents in the React tree of their corresponding withStoreRoot call. Otherwise, we run into a weird case where a root unmounts, the store is GC'd, but there's still a child component holding onto a stale StoreSnapshot prop.

This kind of enforcement is really hard. We can do it at runtime (similar to how React.createContext works), but ideally we want to do it at compile time. Compile time enforcement with a type system doesn't seem to be possible: neither TypeScript nor Flow support recursively constraining JSX child types (microsoft/TypeScript#21699). So we're stuck with runtime enforcement for now.

Exploration here: https://gist.github.com/bcherny/f360f417e8768558684ec3e379f9e354

undux usage outside of react

Hi! I have a cli program that is constantly updating at different intervals. Undux would be a great fit for it, but it is heavily coupled to react. Is there a way to use an undux store outside of a react component, or is something like that in the works? Something like

const store = createIndependentStore({ a: 0, b: 0 })

store.on('a').subscribe(/* do something */)
store.set('a')(1)
store.get('a')

Accessing the store from outside react

How can I get the underlying store from createConnectedStore now that createStore is deprecated? More importantly how do I use this without react now?

Having typing error with typescript and Store.withStore()

image

What can I do to fix this?

Argument of type 'typeof App' is not assignable to parameter of type 'ComponentType<{ store: Store<{ one: number; two: number; }>; }>'.
  Type 'typeof App' is not assignable to type 'StatelessComponent<{ store: Store<{ one: number; two: number; }>; }>'.
    Type 'typeof App' provides no match for the signature '(props: { store: Store<{ one: number; two: number; }>; } & { children?: ReactNode; }, context?: any): ReactElement<any>'.

withReduxDevtools not working after upgrade

After undux upgrade, I'm unable to get withReduxDevtools plugin to work, there is my config.

import { createConnectedStore, withReduxDevtools } from "undux";

let initialState = {
  one: 0,
  two: 0
}
let { withStore, Container } = createConnectedStore(
  initialState,
  withReduxDevtools
)

export default createConnectedStore(initialState, withReduxDevtools);

Proposal to create @undux npm scope

I'd like to propose creating an @undux npm scope in order to remove the current hard dependency of Undux on React.
Instead of making a hard dependency we could split undux up like this:
@undux/core
@undux/react

I already have a working PoC of using Undux in Angular. You can check it out here:
https://github.com/sandervalstar/undux-angular-test
Currently I run a shell script on a postinstall hook which removes the React dependencies from Undux.

I think it should not be too hard to split Undux up into a Core package and a React package, especially in the latest version of Undux (5.0.0-alpha.15) the react dependency already seems pretty well isolated to me.
We'll probably have to create an organization account for Undux on npm to get an @undux scope. Organization accounts on npm are free if you only have public packages.
https://www.npmjs.com/docs/orgs/

Do you think this is a good idea? Will it really be as simple as I think or are there some obstructions that I missed?

By the way, if we agree that this is a good idea I would love to help with splitting up Undux.

To use with react-router?

<Redirect> of react-router-dom is not rendered successfully.
Maybe it seems like this problem when combined with Redux.

I tried to write like this using recompose,

export const withStore = compose(withRouter, connect(store))

but compile error occurs with the callback argument of withStore.

Type '{ children?: ReactNode; }' has no property 'store' and no string index signature.

export default withStore(({ store }) => {
  
}

Is this a problem unique to undux? Or is it a problem on react-router side?

I'm a typecript beginner and not familiar with the type, so I may be wrongfully wrong… If this is a childish question, I'm sorry. 🙇

rxjs_1.Observable.create(...).finally is not a function

Possible noob question, I have the minimum possible Undux store setup but when I call withStore I get that error.

My store (taken from readme example):

import { connect, createStore } from 'undux'

// Create a store with an initial value.
let store = createStore({
    sessionKey: '',
    sessionFetching: false,
})

export let withStore = connect(store);

Where I use withStore:

import React, { Component } from 'react';
import { withStore } from 'utils/store';

const App = withStore(({ store }) =>  // This call causes the error
	<div>
		<h1>gChat</h1>
		<p>Session:{}</p>
	</div>
)

export default App;

The error (x2):

TypeError: rxjs_1.Observable.create(...).finally is not a function
Emitter../node_modules/typed-rx-emitter/index.js.Emitter.createChannel

I'm using regular JS with create-react-app, not typescript; is that the problem? Any advice is much appreciated.

(Also great library, exactly what I was looking for after the verbosity of Redux!)

React-navigation issue

Hi there!

I wonder if I made a mistake of if this is an error from the library...

I'm using react-navigation (https://reactnavigation.org/) and undux.

I tried to wrap the StackNavigator of react-native navigation like this:

import Store from './StatusStore'
...
render() {
const Layout = createRootNavigator(signedIn);

    return(
<Store.Container>    
        <Layout />
</Store.Container>

    )
}

Whereas the createRootNavigator returns a StackNavigator, as follows:

return StackNavigator(
    {
      SignedOut: {
        screen: SignedOut,
        navigationOptions: { tabBarVisible: false, gesturesEnabled: true, swipeEnabled: true}
      },
      SignedIn: {
        screen: SignedIn,
        navigationOptions: { tabBarVisible: false, gesturesEnabled: true, swipeEnabled: true}
      }
    },
    {
      headerMode: "none",
      mode: "modal",
      swipeEnabled: true,
      navigationOptions: {
        gesturesEnabled: true,
        swipeEnabled: true
      },
      //initialRouteName: signedIn ? "SignedIn" : "SignedOut"
      initialRouteName:  "SignedOut"
    }
  );

Also, the Home screen (for example) is exported as follows:

export default Store.withStore(Home);

After that, whenever I try to navigate using the following:

this.props.navigation.navigate($screen)

the app simply does nothing and I can't do anything else. It does not crash nor show any messages, it simply stops.

I want to do that because I need to wrap up the whole app in the store. Do you guys have any idea what this might be?

*also, ty so much for this lib! im finding it awesome! I was using with a parent component taking care of two children and it was working perfectly! I just want to wrap the whole thing up now :(

When setting a value in a store, should the emitter emit if the value has not changed?

As it stands, calling someStore.set('someProperty')(someValue) will always emit, even if someValue is already equal to someStore.get('someProperty') before the call. This can lead to infinite emitting loops in the following circumstance:

tradeTickerStore.on('selectedSymbol')
    .subscribe( symbol => 
        appStore.set('currentSymbol')(symbol) 
    );

appStore.on('currentSymbol')  //update all stores upon setting the general application state
    .subscribe( symbol => {
        orderBookStore.set('symbol')(symbol);
        tradesStore.set('symbol')(symbol);
        candlesStore.set('symbol')(symbol);
        tradeTickerStore.set('selectedSymbol')(symbol); // <-- causes a never ending emit loop.
    });    

While this can be avoided by a conditional clause before calling set('')(), it's convenient it be done inside of set('')() as it avoids having to repeat the condition everywhere it is needed and it also avoids firing an emit event when the value has not changed.

Update readme

The readme still references the now deprecated createStore, and the section on withReduxDevtools doesn't seem correct either.

It might also be useful to link to the specific section on https://undux.org/#api in the readme, for each item listed. Obviously not as big of a deal, but may be nice to have.

Edge case: `createConnectedStore` fails to re-render view

If:

  • Component A has a child component B
  • and B gets and sets field X from the store
  • then A reads X after B has updated X at least once

Then the X that A reads will be out of date with the X that B just set.

Possible fixes:

  • Delegate storeSnapshot.set to storeDefinition.set (this might introduce surprising behavior)
  • Remove the perf optimization
  • ???

Typescript error in connect.d.ts

Using the latest version I'm getting the error below when compiling the project:

node_modules/undux/dist/src/react/connect.d.ts
(7,63): Generic type 'ComponentClass<P>' requires between 0 and 1 type arguments.

React won't re-render when modifying store object or arrays

I have noticed the following behavior

If I set an array or object as a value to given key in the store,

var currentTime = new Date();
let store = withReduxDevtools(
  createStore({
 calendar: {
      year: currentTime.getFullYear(),
      day: currentTime.getDate()
    }
  })
);

If I go about modifying key property year or day inside the store, following this pattern.

 let store = this.props.store;
 
// pull calendar object off the store and update it. 
 let calendar = store.get('calendar');
 calendar.year = 2020;

// update the store
store.set('calendar')(calendar);

In Redux tools, I see the object getting updated, yet react component won't re-render, therefore I had to add a hacky setState to force re-render and make it work.

 this.setState({  test : Math.random() })

am I doing something wrong here?

Thanks

Not really an issue but

Thank you for making this library, if it weren't for this I would have dropped learning react entirely due to all of the over-engineered libraries out there. You guys give me hope that the front-end community can still learn the 'keep it simple' principle ❤️

dealing with new functions created on render

as far as I can tell, putting store.set('key') inside the render function will create a new function on each render call. Based on the reactjs.org/docs/faq-functions.html, that will be considered a new prop to any child component using it. any time the component with the state setting functions renders itself, any child components with those setters will assume a re-render.

Normally, we are encouraged to write our state handlers like so to avoid this:

class App extends React.Component {
  state = { value: null }

  handleChange = value => {
    this.setState({ value })
  }
  render() {
    const { value } = this.state
    return <Widget value={value} onChange={this.handleChange} />
  }
}

For the most part, writing store.set('key') inline will not cause any serious re-renders, but writing inline arrow functions is discouraged for most state libraries for this reason. I ask this because there is a lot of benefit from the nice curried api of undux. Another possibiliy is if the function returned from store.set() was memoized, this could be avoided.
e.g.

class Store {
  returnedSetters = {}
  set = key => {
    if (!this.returnedSetters[key]) {
      this.returnedSetters[key] = value => this.setValue(key)(value) // or whatever actually sets a value here
    }
    return this.returnedSetters[key]
  }
}

Investigate passing specific properties to React components

Instead of passing the entire store.

Note: This will be hard to type safely.

Pros:

  • Let React do the diffing instead of doing it ourselves
  • Avoids the need to watch specific properties for changes and calling forceRender
  • Users can prevent renders with shouldComponentUpdate, and use other lifecycle methods

Alternative: Use an immutable data structure to back store, so the reference changes.

Cons:

  • Asymmetrical API for getting/setting (fixed by #7):

    withStore()(({ foo, bar, set }) =>
     <div onClick={() => set('bar')(3)}>
       {foo}
       {bar}
     </div>
  • Precludes an API where we automatically infer what to watch based on what is used. We can't infer types from the arguments (unless there's a clever trick I haven't thought of), and passing store directly lets us do this

Usage with React Native AsyncStorage

I'm trying to save the store data to the AsyncStorage of my React Native application. Currently, it works well for store -> AsyncStorage data flow, since it's a simple Undux effect.

But how about loading data from the AsyncStorage and getting it back to the store? Is there an "on created" event that I can listen to, or something like that?

Haven't found anything similar in the docs. Thank you!

Immutable.js support?

Currently, diffing process uses just triple equality. It is not suitable in using Immutable#List, because triple equality always returns true even between two different Lists. I would like to get a support of is method from Immutable.js for a right diffing.

ref: https://facebook.github.io/immutable-js/docs/#/is

For further understanding, here is the app on playground using Undux + Immutable.js
https://codesandbox.io/s/kw1vvv5mp5

The point I mentioned in this issue is happening in these lines in todoActions trying updating store but I can't.

    markTodoDone(index: number) {
      const currentTodos = store.get("todos");
      const updatedTodos = currentTodos.update(index, (todo: Todo) => {
        const updatedTodo = todo;
        updatedTodo.markDone();
        return updatedTodo;
      });
      store.set("todos")(updatedTodos);
    }

Objects are not valid as a React child

Trying to run simple hello world. Here are my files:

// store.js
import { connect, createStore } from 'undux'

// Create a store with an initial value.
let store = createStore({
  today: new Date,
  users: []
})

export let withStore  = connect(store)
// App.js
import React, { Component } from 'react';
import { withStore  } from './Store'

// Update the component when the store updates.
let App = withStore (
  class extends React.Component {
    render() {
      let store = this.props.store;
      return (<div>
        Hello! Today is {store.get('today')}
        <button onClick={() => store.set('today')(new Date)}>Update Date</button>
      </div>)
    }
  }
)

export default App;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
 
ReactDOM.render(<App />, document.getElementById('root'));

This is the error I get:

Objects are not valid as a React child (found: Sat Jun 02 2018 16:37:40 GMT+0400 (Georgian Standard Time)). If you meant to render a collection of children, use an array instead.
in div (at App.js:9)
in _class (created by withStore(_class))
in withStore(_class) (at index.js:5)

Any idea why?

Is it possible to update partial values?

Hi! Thank you for a wonderfully simple and type-safe library. 😄

I thought about this kind of things.
Currently, it seems that I can not set without passing all the properties to set, but I'd like to update the value for partially.

For example, I would like to write the following code.

interface Some {
    one: string
    two: string
}

interface AppStore {
     some: Some
     other: string
}

store.set("some")({one: "hoge"}) // <- Is this possible? Do you have plans to make it possible if you can not? 

Interface improvement request: store.get.someProp instead of store.get('someProp')

babydux looks interesting - but i'm not a huge fan that a string literal is used to get a value from the store. example (store.get('someProp')).

I would prefer to have type safety on the store and the prop getters and setters.
It would be ideal to call via a getter store.get.someProp or simply store.someProp with compile time type safety for the getters and setters.

This could be done with ES6 Proxy

There are also some proxy polyfill options out there for backward compatibility.

Why do you develop alpha version on master branch?

Hi. I appreciate the development of undux.

As a simple question, why are you developing the alpha version on the master branch? Do you have any special reasons?
Similarly, publishing to npm doesn't use the alpha tag. So alpha version is installed with npm install undux command.

I regularly keep track of this repository's commit logs and issues, so I notice changes in the API and master are alpha versions. But many people are not. Especially I think that it will cause confusion for people who use undux for the first time.

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.