Coder Social home page Coder Social logo

rematch / rematch Goto Github PK

View Code? Open in Web Editor NEW
8.5K 106.0 420.0 23.47 MB

The Redux Framework

Home Page: https://rematchjs.org

License: MIT License

JavaScript 3.66% TypeScript 92.06% CSS 4.23% Shell 0.06%
redux rematch react vue angular javascript typescript react-native immer react-redux

rematch's Introduction

Rematchjs

Rematch is Redux best practices without the boilerplate. No more action types, action creators, switch statements or thunks in less than 1.4 kilobytes.


Chat on Discord Rematch CI Bundle size File size lerna https://img.shields.io/lgtm/grade/javascript/github/rematch/rematch?logo=typescript Gitpod Ready-to-Code

Documentation · Quickstart · Examples · Contribute · Licence

Features

Redux is an amazing state management tool, supported by a healthy middleware ecosystem and excellent devtools. Rematch builds upon Redux by reducing boilerplate and enforcing best practices. It provides the following features:

  • No configuration needed
  • Reduces Redux boilerplate
  • Built-in side-effects support
  • React Devtools support
  • TypeScript support
  • Supports dynamically adding reducers
  • Supports hot-reloading
  • Allows to create multiple stores
  • Supports React Native
  • Extendable with plugins
  • Many plugins available out of the box:

Are you ready to use Rematch?

In a few lines you can get easily asynchronous calls to an external API and data stored globally. It's amazing, with Redux you will needs tons of boilerplate, libraries and extra configuration.

type PlayersState = {
    players: PlayerModel[]
}

export const players = createModel<RootModel>()({
    state: {
        players: [],
    } as PlayersState,
    reducers: {
        SET_PLAYERS: (state: PlayersState, players: PlayerModel[]) => {
            return {
                ...state,
                players,
            }
        },
    },
    effects: (dispatch) => {
        const { players } = dispatch
        return {
            async getPlayers(): Promise<any> {
                let response = await fetch('https://www.balldontlie.io/api/v1/players')
                let { data }: { data: PlayerModel[] } = await response.json()
                players.SET_PLAYERS(data)
            },
        }
    },
})

Check it out, right now!

Redux vs Rematch

Redux Rematch
simple setup ‎ ‎✔
less boilerplate ‎✔
readability ‎✔
configurable ‎ ✔ ‎✔
redux devtools ‎✔ ‎✔
generated action creators ‎✔
async thunks ‎async/await

Migrate From Redux

Migrating from Redux to Rematch may only involve minor changes to your state management, and no necessary changes to your view logic. See the migration reference for the details.

Composable Plugins

Rematch and its internals are all built upon a plugin pipeline. As a result, developers can make complex custom plugins that modify the setup or add data models, often without requiring any changes to Rematch itself. See the plugins developed by the Rematch team or the API for creating plugins.

Contact & Support

Contributors

Thank you to all the people who have already contributed to rematch!

Made with contributors-img.

Licence

This project is licensed under the MIT license.

rematch's People

Contributors

arcthur avatar arthurjdam avatar blairbodnar avatar bosung90 avatar brunolemos avatar crazy4groovy avatar d3dc avatar dcousens avatar dependabot[bot] avatar eeynard avatar hardfist avatar iosh avatar jackwilsdon avatar jancassio avatar jaywcjlove avatar kamerontanseli avatar koss-lebedev avatar linonetwo avatar loveky avatar mathieutu avatar michallytek avatar millette avatar pierissimo avatar ricosmall avatar saginadir avatar semoal avatar shmck avatar taranda avatar tianzhich avatar titouancreach 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rematch's Issues

Should effects be actions?

Looking at this bit of code makes me wonder. I believe its possible to create effects that don't require actions, and therefore no middleware.

// creates a bound function with the context of dispatch[model.name]
effects[`${model.name}/${effectName}`] = model.effects[effectName].bind(dispatch[model.name])dispatch[model.name][effectName] = 

// action is dispatched and later picked up in middleware
createDispatcher(model.name, effectName)

On one hand, this would restrict actions to only things that change state. Effects would act like regular functions.

On the other hand, you don't get a record of effects in your devtools.

Is this even a good idea?

look into a way to reference "this" within a model

When referencing state within an effect, it would be nice if we could reference state with something like:

reducers: {
  total: 0
},
effects: {
  add: (payload) => this.total + payload
}

Is this an idea worth considering?

Root Reducer Enhancer

Add a root reducer enhancer for implementing the new "redux-persist" 5.0.

Currently there doesn't seem to be a way to do this in DVA, so I'm thinking of moving over soon.

rename items

Internal:

reduce => reducers
effect => effects

External:

view => select

Unsure why test fails

I'm not sure why the following plugins test is failing in "plugins.test.js".

 xtest('should add middleware hooks', () => {
    const m1 = () => next => action => next(action)
    const m2 = () => next => action => next(action)
    const hooks = [{
      middleware: m1,
    }, {
      middleware: m2
    }]
    createPlugins(hooks, [])
    expect(pluginMiddlwares).toEqual([m1, m2])
  })

Test hooks with multiple listeners

Possible bug:

I believe in the current form, new hooks may overwrite existing hooks.

TODO
  • write tests with multiple hooks of the same name

Rename hooks

Hooks doesn't really describe what "hooks" do, may be better to rename them. Some ideas:

  • subscriptions
  • subs
  • listeners

Selectors API

As noted in #50:

We should ensure the selectors API should:

  • Integrate easily with the view layers. Eg react-redux's connect
  • Integrate easily with reselect / or other similar techniques to make them more efficient (and maybe composable)

A single "core" middleware with hooks

It may be more performant for core features with middleware to be looped over within a single core middleware.

This could be an additional option for middleware. On the other hand, it might complicate the API.

use es6 map over objects when dynamic adding/removing

According to MDN

Use maps over objects when keys are unknown until run time, and when all keys are the same type and all values are the same type

This may be a good use case for using ES6 maps over objects for "dispatch", "effects" & "hooks".

We are essentially adding and removing these keys on runtime.

Selectors and React example implemented... And I accidentally pushed my branch to master :(

@ShMcK I accidentally pushed my branch to master 😞. Sorry about that. I had created a branch and had accidentally set the upstream to origin/master on it. I didn't realize my push would be to master. Ugh! I was all ready for a nice PR!

If you want to review the commits for this "PR", it's the first 6 commits I did today, Oct 14.

Anyways, here is what I was going to say in the PR:

  • Selectors are not tied to views anymore
  • Now that getStore is exposed, I don't think we need to provide a 'view' in init anymore. (See example below). Maybe complex view implementations can be done via plugins or something similar... But maybe it's not even needed! For example, look at the react-redux implementation below!

From examples/react-redux-count/

import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { init, model, dispatch, select, getStore } from 'rematch-x'

// No need to specify a 'view' in init.
init()

// Create the model
model({
  name: 'count',
  state: 0,
  reducers: {
    increment: state => state + 1
  },
  selectors: {
    doubled: state => state * 2
  }
})

// Make a presentational component.
// It knows nothing about redux or rematch.
const App = ({ value, valueDoubled, handleClick }) => (
  <div>
    <div>The count is {value}</div>
    <div>The count doubled is {valueDoubled}</div>
    <button onClick={handleClick}>Increment</button>
  </div>
)

// Use react-redux's connect
const AppContainer = connect(state => ({
  value: state.count,
  valueDoubled : select.count.doubled(state),
  handleClick: () => dispatch.count.increment()
}))(App)

// Use react-redux's <Provider /> and pass it the store.
ReactDOM.render(
  <Provider store={getStore()}>
    <AppContainer />
  </Provider>,
  document.getElementById('root')
)

Remove lib ?

Most libraries tend to remove the "lib" directory, and only create it on "npm run build" to generate the new library. This prevents a lot of unnecessary commit diffs and potential merge conflicts from a frequently updated and minified build.

I think we should do the same.

However, it will ruin the local npm links. You'll have to run "npm run build" when testing.

Or temporarily change the root "package.json" "main" to reference "src".

selectors auto bind to getState()

Should selectors automatically be passed getState() as the first parameter?

Current
state = 1
select.count.double(state) = 2
Proposed
state = 1
select.count.double() = 2

Current
state = { '2': { name: 'Joe' } }
select.user.getById(state, '2') = { name: 'Joe' }
Proposed
state = { '2': { name: 'Joe' } }
select.user.getById('2') = { name: 'Joe' }

Rethinking `effects`

Hmm.

I wonder... do effects really belong in a model?

Effects... they do a bunch of impure stuff... and then they call dispatch, multiple dispatches, or not even call dispatch at all. The only real relation with a model is that they sometimes call a dispatch for a model.

The more I think about it, the more I start leaning towards something like this...

Idea A: effects implemented in init.

import { init } from 'rematch'
init({
  effects: {
    doStuff: async (payload, getState) => {...}
  }
})

// elsewhere
import { effects } from 'rematch'
effects.doStuff({ foo: 'bar' })

Idea B: effects are 'registered' like a model

import { effect } from 'rematch'
effect({
  doStuff: async (payload, getState) => {...}
})

// elsewhere
import { effects } from 'rematch'
effects.doStuff({ foo: 'bar' })

Thoughts?

Docs

It seems like publishing docs is coming up. A good solution should:

  • use markdown
  • be easy to learn
  • not look terrible

A suggestion for publishing our own docs.

This looks like the path of least resistance: Github Pages with Jekyll. I made something similar with this site: https://coderoad.github.io/tutorial-docs.html

I would however, prefer if the doc templates were in a component based framework, rather than Jekyll.

Any other suggestions?

jest bug? This `effects` test actually passes... just not in jest

I've been trying to fix this one failing effects test:

  test('should be able to trigger another action w/ multiple actions', async () => {
    init()

    model({
      name: 'example',
      state: 0,
      reducers: {
        addBy: (state, payload) => state + payload,
      },
      effects: {
        asyncAddOne: async () => {
          await dispatch.example.addBy(1)
        },
        asyncAddThree: async () => {
          await dispatch.example.addBy(3)
        },
        asyncAddSome: async () => {
          await dispatch.example.asyncAddThree()
          await dispatch.example.asyncAddOne()
          await dispatch.example.asyncAddOne()
        }
      }
    })

    await dispatch.example.asyncAddSome()

    expect(getStore().getState()).toEqual({
      example: 5,
    })
  })

It's really odd - The code in the test actually works fine! I tested it out in 'production' and got the results I'd expect when calling dispatch.example.asyncAddSome(). (I tested with a create-react-app).

Anyways, I spent some time debugging it and for some reason jest only executes the first two lines of any effect. It's really odd! I'm taking a break from it now... this is a tricky bug!

I'll remove the test from the master branch for now because all other tests are passing. We can add this test back in once we figure out what is going on here.

export dispatch as a function

This question is blocking #3. We need a way to export dispatch as a function that can later have items added to it like an object.

Functions

Note that:

export let a

a = () => console.log('dispatch')
a.b = {}
a.b.c = () => console.log('dispatch(b/c)')

a()
a.b.c()

But imports are read only and run on start.

import { a } from './file'

a
// undefined
Objects

Objects do not have the same issue.

export let a = {}
a.b = {}
a.b.c = () => console.log('dispatch(b/c)')

a()
a.b.c()

But imports are read only and run on start.

import { a } from './file'

a
// { a: { b: { c: function } } }

Model API change? A different way to specify name and state?

I'm wondering about simplifying model names and state even further...

Current
model({
  name: 'count',
  state: 0
})

model({
  name: 'todos',
  state: []
})
Proposed
model({
  count: 0
})

model({
  todos: []
})

Then, in createModel we could determine what that "special" key is to derive the model name and initial state.

Something like this...

const corePluginModelKeys = () => ['reducers', 'effects', 'selectors', 'hooks']
const otherPluginModelKeys = () => { // TODO get model keys that other plugins have introduced }

const createModel = (model: $model): void => {
  const pluginKeys = [...corePluginKeys(), ...otherPluginModelKeys()]
  const specialKey = Object.keys(model).find(key => !pluginKeys.includes(key))
  
  const modelName = specialKey
  const state = model[modelName]
 
  // continue on with createModel
 ...
}
Pros
  • Less typing
Cons
  • Can't use any of the model plugin keys as model name
  • A little bit harder to migrate from mirror / dva?
  • Harder to understand for the user?

actions return a promise with Promise middleware

This is very useful when doing things like navigating away after an action is completed.

dispatch.something.do()
.then(() => dispatch.navigate.somePage())

Can be handled with Redux Promise middleware. See DVA example.

It may be better to implement our own Promise middleware than to depend on an additional package.

Plugin API finalization

I recently tried to write a "loading" plugin, but found it quite hard with the current API.

There are two changes I propose:

  1. all plugins should be functions (rather than objects)
  2. pass storeDispatch in as the first? param
  3. pass a config in as the second? param

This is a little simpler, though I'm not entirely happy with it yet. See an outline of changes below:

const plugin = (storeDispatch: $dispatch, config: $pluginConfig) => ({
  onInit() {},                          // removed storeDispatch as param
  onModel(model: $model) {}             // removed storeDispatch as param
  middleware: (store) => (next) => (action) => {}
})

I'm not sure what yet constitutes a plugin config, but we can figure that out.

I think this requires more consideration, and we're likely to learn a lot more by making a list of possible plugins and which internals they would need access to.

Better commented code

We could use more code comments. Should read more like a book.

It might also be helpful if we had some kind of a standard for commenting code.

Handle arrow functions when binding this

Arrow functions are the only types of functions not handled with effects. See #7 for efforts.

Options:

  1. convert arrow functions to regular functions
  2. console.warn users not to use arrow functions
  3. wrap arrow functions in a function that provides the dispatch context

Unfortunately, there doesn't seem to be a way to accurately detect arrow functions.

onError

Should handle global errors. The following should work.

init({
  onError: (e) => console.log(e)
})

Meta Actions

Allow for actions to pass a "meta" property around.

Currently we only pass around "type" and "payload". This may be necessary for a "redux-offline" or "redux-optimist" style plugin.

Are action creators legacy?

Dispatch is basically made of a bunch of action creators organized by model.

See createDispatcher

The current design potentially offers the following benefits

  • potential autocomplete for dispatches (not really working)
  • immediate referencing of the action creator

Idea 1: Hacky JavaScript ?

But if all action creators are all essentially the same code, is there a way to create action creators on the fly?

dispatch.count.addBy(5)
// someone how captures "count" as model
// someone captures "addOne" as the action
// somehow captures the param "5" as payload

Although, I don't think the above would work dynamically,

Idea 2: Dispatch as a Function

it's possible to reference dispatches in a way that would work. Some examples:

dispatch('count', 'addBy', 5)
dispatch('count/addBy', 5)

This would call dispatch['count']['addBy'](5). Boom.

Or some variation of above.

The only benefit really being that you don't have to create action creators on startup, and hold them in memory through the life of the app. Note that this is what most standard redux apps do anyway, it just strikes me as wasteful.

I'm not sure how you would structure this if you were adding extra properties to the action, like "meta". But we don't currently handle any situations like that anyway.

dispatch('count/addBy', 5, { meta: something: true })

Summary

Consider this more of a thought experiment than a proposal.

plugins API ?

We should create a plugins API under init. This can allow for merging store middleware, setting up models, etc.

Plugins may be:
A) an array of objects

init({
  plugins: [plugin(), plugin2()]
})

B) an array of strings that internally references an existing local package

init({
  plugins: ['plugin1', 'plugin2']
})

C) Another possible API to consider, as used by DVA:

const app = init()
// sets up plugin
app.use(plugin())

Regardless, we should decide on a plugin API with some reasoning.

Some things to consider:

  1. The plugins are more efficient if they run on startup, which leads me towards A & B.
  2. If we are to hold all packages locally, I'd lean towards B. If users are able to create their own packages, or add config settings to a package I'd go with A.

Personally, I'm leaning towards A. What are your thoughts?

Format of action

In dva/mirror, actions are generally lower cased: "model/action".

However, I wonder which of the following makes the most sense as a best practice:

  • "model/actionName"
  • "MODEL/ACTION_NAME"
  • "Model/ActionName"
  • etc.

Consider Zenhub for project boards

@blairbodnar, to enable Zenhub:

  1. get the Zenhub chrome extension

  2. reload Github

  3. Click on "boards" instead of "projects"

Items will automatically move to categories based on their status.

For example, a PR will move into "Q/A under review", and a closed issue will automatically move to "Completed".

It's a bit more featured that the basic Github boards.

Batch updates

I think we should consider this as a feature or plugin.

Batching updates can help increase performance. When a lot of actions are triggered within a very short period of time, its more performant if the state is only calculated once.

An implementation.

Docs

(Thoughts sorted out with @blairbodnar.)

Tagline

"Rematch. Rethink Redux"

Purpose

Say this is a few sentences so anybody can understand.

  • data layer. wrapper around redux.

  • better app organization

  • simplifies better app architecture

  • easier API

  • easier refactoring

  • does the same thing

  • opinionated about Redux

  • not opinionated about the other stuff you do

  • for small apps made easier, big apps made simpler

Built In

  • store setup
  • devtools

Example

  • A complete example

Examples with view layer

  • React
  • Vue
  • Preact
  • jQuery
  • vanilla JS
  • ember?
  • angular?

Setup

Installation

API

Level 1:
  • init
  • model
  • name
  • state
  • reducers
Level 2:
  • effects
  • selectors
  • subscriptions
Level 3:
  • plugins

Inspiration

Dva, Mirror.

'Recipes' for reducers, selectors, etc?

@ShMcK @scottclayton

I'm just brainstorming... Not sure if this is something that we want to rematch to help with or not. But I'm wondering if it would be nice to have some pre-made 'recipes' for common state designs? Not sure if 'recipes' is the right word here... hmm.

@ShMcK We briefly chatted about this the other day. I think I said something along the lines of "I use a lot of similar reducers and selectors, I want to reuse them!". And you said something along the lines of "What if we use object spread?"

So, here's a brain-dump:

I find that as I develop my apps, I tend to come up with a state structure for common things. For example, I end up using the same state structure when dealing with a collection...

Someone: "Make me a todo-list"
Me: "Ok"
My brain:

./models/todos.js

model({
  name: 'todos',
  // I might have state like this. I kinda like this pattern of `state.id.item`
  state: {
    '2fc9acde': {
      text: 'release rematch to the wild',
      done: false
    },
    'fb6bb0e4': {
      text: 'drink coffee',
      done: true
    }  
  },
  // with reducers like this...
  reducers: {
    addItem: (state, payload) => { ... },
    removeItem: (state, payload) => { ... },
    updateItem: (state, payload) => { ... },
    clearList: (state, payload) => { ... },
    // etc.
  }
  // and selectors like this...
  selectors: {
    getItem: (state, payload) => { ... },
    findById: (state, payload) => { ... },
    getIds: (state) => { ... },
    getItemProperty: (state, payload) => { ... },
    // etc.
  }
})

Later on...

Someone: "Make me a shopping cart"
Me: "Ok"
My brain: "Hey, I liked the way I dealt with the collection in my todos model. I think I'll use the same design for this part of my shopping cart..."

So, maybe I move my reducers and selectors somewhere so they can be reused...

./recipes/collection.js

export const reducers = {
  addItem: (state, payload) => { ... },
  removeItem: (state, payload) => { ... },
  updateItem: (state, payload) => { ... },
  clearList: (state, payload) => { ... },
  // etc.
}

export const selectors = {
  getItem: (state, payload) => { ... },
  findById: (state, payload) => { ... },
  getIds: (state) => { ... },
  getItemProperty: (state, payload) => { ... },
  // etc.
}

And then use them in models that deal with collections with the same state structure.

./models/todos.js

import { reducers, selectors } from './recipes/collection'
model({
  name: 'todos',
  state: {
    '2fc9acde': {
      text: 'release rematch to the wild',
      done: false
    },
  },
  reducers: { ...reducers },
  selectors: { ...selectors },
})

./models/shoppingCart.js

import { reducers, selectors } from './recipes/collection'
model({
  name: 'shoppingCart',
  state: {
    '9bd1e596': {
      item: 'reese peanut butter cups',
      quantity: 99
    },
  },
  reducers: {
    ...reducers,
    anotherReducer:  (state, payload) => { ... },
    // etc.
  },
  selectors: {
    ...selectors,
    anotherSelector:  (state, payload) => { ... },
    // etc.
  },
})

Examples

Demos:

JavasScript

  • counter
  • todo

React

  • counter
  • todo

Counter

Super basic.
Only reducers.

Todo

Advanced.
Reducers, Effects, Selectors, Subscriptions.

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.