rematch / rematch Goto Github PK
View Code? Open in Web Editor NEWThe Redux Framework
Home Page: https://rematchjs.org
License: MIT License
The Redux Framework
Home Page: https://rematchjs.org
License: MIT License
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.
Hooks doesn't really describe what "hooks" do, may be better to rename them. Some ideas:
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.
It should be possible to create a plugin that adds a model. For example "loading".
It seems like publishing docs is coming up. A good solution should:
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?
Possible bug:
I believe in the current form, new hooks may overwrite existing hooks.
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.
Modify hooks to use a "this" context within the model
I recently tried to write a "loading" plugin, but found it quite hard with the current API.
There are two changes I propose:
storeDispatch
in as the first? paramThis 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.
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.
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?
As an example:
dispatch({ type: 'example/something' })
dispatch.example.something()
Both should work.
Remove the shortcuts. Eg.
export {
init,
init as i,
model,
model as m,
...
}
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.
Demos:
Super basic.
Only reducers.
Advanced.
Reducers, Effects, Selectors, Subscriptions.
Internal:
reduce => reducers
effect => effects
External:
view => select
Arrow functions are the only types of functions not handled with effects. See #7 for efforts.
Options:
console.warn
users not to use arrow functionsUnfortunately, there doesn't seem to be a way to accurately detect arrow functions.
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.
Look into a hooks API for models.
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.
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 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 } } }
@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:
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!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')
)
Dispatch is basically made of a bunch of action creators organized by model.
The current design potentially offers the following benefits
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,
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 })
Consider this more of a thought experiment than a proposal.
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:
Personally, I'm leaning towards A. What are your thoughts?
If a user has a subscription that matches a reducer or effect in the same model, its possible, and even likely it will result in an infinite loop. This is definitely a not good thing.
As noted in #50:
We should ensure the selectors API should:
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?
I'm wondering about simplifying model names and state even further...
model({
name: 'count',
state: 0
})
model({
name: 'todos',
state: []
})
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
...
}
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.
Hooks should trigger a warning while in development mode if they are calling outside of the model.
Currently removed. Tests are in place but skipped.
See #11 for notes.
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...
effects
implemented in init
.import { init } from 'rematch'
init({
effects: {
doStuff: async (payload, getState) => {...}
}
})
// elsewhere
import { effects } from 'rematch'
effects.doStuff({ foo: 'bar' })
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?
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".
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.
@blairbodnar, to enable Zenhub:
get the Zenhub chrome extension
reload Github
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.
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:
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:
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...
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.
import { reducers, selectors } from './recipes/collection'
model({
name: 'todos',
state: {
'2fc9acde': {
text: 'release rematch to the wild',
done: false
},
},
reducers: { ...reducers },
selectors: { ...selectors },
})
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.
},
})
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])
})
Create a persist plugin using redux-persist.
(Thoughts sorted out with @blairbodnar.)
"Rematch. Rethink Redux"
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
Installation
Dva, Mirror.
Should selectors automatically be passed getState()
as the first parameter?
state = 1
select.count.double(state) = 2
state = 1
select.count.double() = 2
state = { '2': { name: 'Joe' } }
select.user.getById(state, '2') = { name: 'Joe' }
state = { '2': { name: 'Joe' } }
select.user.getById('2') = { name: 'Joe' }
Modify API to handle take once & take every hooks.
Should handle global errors. The following should work.
init({
onError: (e) => console.log(e)
})
Create a loading plugin similar to mirror-loading.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.