This method is used to create and inject a model. A "model" is a combination of Redux's state
, action
and reducer
. Calling mirror.model
will automatically create actions and reducers, which will be used to create Redux
store.
Basically, it's a simple and powerful way to organize you Redux
stuff.
To create a model, name
must be provided and be a valid string. It is the name of the model, which means it will be used as the namespace of the future-to-create Redux
store.
Suppose you create a model like this:
import mirror from 'mirrorx'
mirror.model({
name: 'app',
})
Then you will get a Redux
store like this:
// ...
store.getState()
// {app: null}
The model name
is where your Redux
state goes in you root store(of course, it's important to actions
too, we'll cover that later).
Also note that the value of the created store's app
state is null
, if you want a different, more meaningful value, then you need to pass an initialState
.
Note: Mirror uses react-router-redux, so you can not use
routing
as model name.
As its name indicated, initialState
is the initial state of a model, nothing special. It is used as the initialState
of a standard Redux
reducer.
It is not required, and could be anything. If initialState
is not specified, then it will be null
, as demonstrated above.
Create model:
import mirror from 'mirrorx'
mirror.model({
name: 'app',
+ initialState: 0,
})
After store is created:
store.getState()
// {app: 0}
reducers
is where you put your Redux
reducers. The principle here is one reducer, one action, so you don't need to care about the action type you are dealing with.
-import mirror from 'mirrorx'
+import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'app',
initialState: 0,
+ reducers: {
+ add(state, data) {
+ return state + data
+ },
+ },
})
Execute the code above, Mirror will do 3 things behind the scenes:
- Create a reducer;
- Create a action type (
app/add
in this case), which will be captured by the created reducer; - Add a function whose name is the reducer's name under
actions.<modelName>
object. This function, when called, willdispatch
the very action created above.
Here, we can see that model's name
has another usage:
// ...
typeof actions.app
// 'object'
typeof actions.app.add
// 'function'
actions.app.add(1)
// Same as:
//
// dispatch({
// type: 'app/add',
// data: 1
// })
// ...
store.getState()
// {app: 1}
Yes, model name
will be an attribute of the actions
object, who it self is an object too. And all functions you defined in reducers
will be added as methods to that object under the same name.
Functions defined in reducers
are most of the part a Redux
reducer(so it must be a pure function too), except one tiny difference:
// Redux standard reducer
function reduxReducer(state, {type, data}) {
// do something, return some other state
}
// reducer defined in `reducers`
function reducerInReducers(state, data) {
// do something, return some other state
}
For the standard Redux
reducer, you pass an action object as the second parameter; while for the "reducer" defined in model's reducers, you pass the action data as the second parameter, because you don't have to care about the action type -- Mirror does that for you.
What parameter should you pass when calling methods added in actions.<modelName>
? Just the action data.
// ...
// You don't need to pass a `state` when dispath actions, right?
actions.app.add(100)
Every reducer of every model you created will be combined together(using Redux's combineReducers
), and then used to create your Redux
store.
Note: non-function entries in
reducers
is pointless, and will be ignored(same aseffects
):import mirror, {actions} from 'mirrorx' mirror.model({ name: 'app', reducers: { a: 1 }, }) actions.app // undefined
effects
are async actions of Redux. In functional programming, effect
is the interaction with the world outside of a function. Since async actions do interact with the outside world, so they surely are effects.
An effect
does not directly update your Redux
state, but invokes other "sync actions" to update the state, usually after some asynchronous operations(like HTTP requests).
Like reducers
, every function you defined in effects
will be added to actions.<modelName>
as a method with the same name, and calling this method will call the original function.
import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'app',
initialState: 0,
reducers: {
add(state, data) {
return state + data
},
},
+ effects: {
+ async myEffect(data, getState) {
+ const res = await Promise.resolve(data)
+ actions.app.add(res)
+ }
+ },
})
Now, actions.app
will have 2 methods:
actions.app.add
actions.app.myEffect
There is no magic here, calling actions.app.myEffect
will dispatch an action and run the exact code in effects.myEffect
:
// ...
// First, dispatch the action:
// dispatch({
// type: 'app/myEffect',
// data: 10
// })
//
// Second, invoke the method:
// effects.myEffect(10)
actions.app.myEffect(10)
// ...
store.getState()
// {app: 10}
That's it, all you have to do is call the methods Mirror automatically added to actions.<modelName>
, and your async actions is dispatched! Maybe you have used some great middlewares to handle async actions, such as redux-thunk or redux-saga. But none of them is as simple as Mirror is.
Functions you defined in effects
will get 2 arguments:
data
- The data you pass when calling methods inactions.<modelName>
.getState
- It's actuallystore.getState
, will return the root state of your store when called.
But, when calling the corresponding methods Mirror added to actions.<modelName>
, you only need to pass the above data
parameter to it -- if you want to.
async/await
is the recommended way to define effects, but is not the only way.
You can go Promise
:
// ...
effects: {
promisedEffect(data, getState) {
return Promise.resolve(data).then(result => {
// call your sync actions
})
}
}
Or, you can even go the old school callback
(discouraged):
// ...
effects: {
callbackEffect(data, getState) {
setTimeout(() => {
// call your sync actions
}, 1000)
}
}
The point is, you can handle your async operations in whatever way you want, Mirror provides a consistent API to manage them.
Note: action name in
effects
should not be duplicated with those inreducers
:import mirror, {actions} from 'mirrorx' // Will throw an error mirror.model({ name: 'app', reducers: { add(state, data) { } }, effects: { add(data, getState) { } } })
The actions
object contains both your Redux action
s and reducer
s. Calling methods in it will dispatch
some secret action, which will be captured by functions you defined in reducers
and effects
object.
In Mirror, all actions and effects are generated automatically and "namespaced”, meaning, you can't manually create an action
, and more importantly, you don't have to.
You don't have to explicitly create and dispatch any action at all. If you want to create an action
and a reducer
to handle it, don't bother to add an action type constant
(or an action creator
), and then add a reducer
, just throw a reducer in reducers
, that's all.
Thus, you don't have to jump through files or directories to determine which action type should be handled by which reducer.
For example, run:
actions.app.add(1)
Is exactly the same as the following code:
dispatch({
type: 'app/add',
data: 1
})
Plus, using this global actions
to handle Redux
actions, you can easily tell the "dependencies" between different modules:
In a.js:
// a.js
import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'a',
initialState: 0,
reducers: {
add(state, data) {
return state + data
},
},
})
In b.js:
// b.js
import mirror, {actions} from 'mirrorx'
mirror.model({
name: 'b',
effects: {
async foo(state, data) {
const res = await Promise.resolve(data)
// update state of model `a`
actions.a.add(data)
},
},
})
If the enhanced Router
component provided by Mirror is used in your app, then you'll get actions.routing
for free.
There are 5 methods in actions.routing
:
push(location)
- Pushes a new location to history, becoming the current location.replace(location)
- Replaces the current location in history.go
- Moves backwards or forwards a relative number of locations in history.goForward
- Moves forward one location. Equivalent to go(1).goBack
- Moves backwards one location. Equivalent to go(-1).
The usage of these methods are exactly the same with history API. And thanks to react-router-redux, an action will be dispatched when called, so your history will be synced with your store.
import mirror, {actions} from 'mirrorx'
// ...
actions.routing.push('/foo/bar')
// => http://example.com/foo/bar
actions.routing.push({
pathname: '/foo/bar',
search: '?search=123'
})
// => http://example.com/foo/bar?search=123
You can learn more from here.
Note: if your app does not use
Router
,actions.routing
would beundefined
.
Add a hook to monitor actions that have been dispatched.
import mirror, {actions} from 'mirrorx'
// ...
const locationChangeHook = mirror.hook((action, getState) => {
if (action.type === '@@router/LOCATION_CHANGE') {
console.log('Location has just changed')
}
})
const countHook = mirror.hook((action, getState) => {
if (getState().app.count === 10) {
console.log('You have just reached 10!')
}
})
// Remove hooks
locationChangeHook()
countHook()
mirror.defaults
is a pretty intuitive API, you use it to configure your Mirror app.
mirror.defaults
can be called multiple times.
- Default:
undefined
The preloadedState
for your Mirror app's store.
mirror.defaults({
initialState: {app: 1}
})
mirror.model({
name: 'app',
// ...
})
// ...
store.getState()
// {app: 1}
- Default:
browser
The history type for your router, there are 3 optional values:
browser
- A DOM-specific implementation, useful in web browsers that support the HTML5 history API.hash
- A DOM-specific implementation for legacy web browsers.memory
- An in-memory history implementation, useful in testing and non-DOM environments like React Native.
For more information, check out the history package.
- Default:
[]
Specifies a list of Redux middleware.
This option is useful if you want to use some third party middlewares. In this case, you have to connect
without mapDispatchToProps
specified to get props.dispatch
method, so you can dispatch actions manually.
- Default:
{}
Specifies some custom reducers. Reducers defined here must be standard Redux reducers, and this will be directly handled by combineReducers
.
For example, to use redux-form, you can add redux-form's reducer as the following:
import mirror from 'mirrorx'
import { reducer as formReducer } from 'redux-form'
mirror.defaults({
reducers: {
form: formReducer
}
})
There's something special about options.reducers
, that is its key-value
s will be merged into the previous ones instead of replacing them, since you can call mirror.defaults
multiple times. That's the case when you call it after your app has been started, for example:
// after this call, your store will hava a standard reducer with namespace
// of `a`
mirror.defaults({
reducers: {
// standard Redux reducer
a: (state, data) => {}
}
})
// ...
// then somewhere in your app, you can add other standard Redux reducers
mirror.defaults({
reducers: {
// standard Redux reducer
b: (state, data) => {}
}
})
After the second call, your store will have 2 reducers: a
and b
.
- Default:
(effects) => (name, handler) => { effects[name] = handler }
Presents an option to configure how effects are created. Checkout mirror-saga for more information.
connect
connects your React
component to your Redux
store. This is exactly the same connect
method from react-redux.
You must connect
your component if it needs the data from your store; but it's not necessary to connect
if your component only wants to dispatch Redux
actions, because the actions
object is accessible everywhere in your app, even your presentational components.
Note: if you
connect
your component withoutmapDispatchToProps
specified, then you'll getprops.dispatch
, which gives you the power to use some third party middlewares. This is the only case you should manually calldispatch
in you component, in other cases, always use methods inactions
to dispatch actions.
render
is an enhanced ReactDOM.render
, it starts your Mirror app.
It first creates your Redux
store, then renders your component to DOM using ReactDOM.render
。render
takes exactly the same parameters as ReactDOM.render
does.
You can call render
multiple times in your app. The first time being called, render
will create a Redux
store using all the reducers
and effects
you defined through mirror.model
method. After that, all later calls will replace your store's reducer and re-render your app.
What's the point of that? It allows you to inject models dynamically, it's very convenient for code-splitting.
For example, suppose you have an app.js
:
// app.js
import React from 'react'
import mirror, {actions, connect, render} from 'mirrorx'
mirror.model({
name: 'foo',
initialState: 0
})
const App = connect(({foo, bar}) => {
return {foo, bar}
})(props => {
return (
<div>
<div>{props.foo}</div>
<div>{props.bar}</div>
</div>
)
})
render(<App/>, document.getElementById('root'))
After render
, your app will be rendered as:
<div>
<div>0</div>
<div></div>
</div>
Then, suppose you have an async component/model which can be loaded by tools like react-loadable:
// asyncComponent.js
// inside this async component, you define an "async model"
mirror.model({
name: 'bar',
initialState: 'state of bar'
})
// app.js
// ...
// some where in your app, after loading above component and model,
// call `render()` will "register" the async model and re-render your app.
//
// NOTE: the `load` function is NOT a real implementation, it's just pseudo code.
load('ayncComponent.js').then(() => {
mirror.render()
})
Calling render
without arguments will re-render your app. So above code will generate the following html
:
<div>
<div>0</div>
- <div></div>
+ <div>state of bar</div>
</div>
Plus, after the "async component/model" has been loaded, it's possible to call mirorr.defaults
to add some standard Redux reducers on the fly:
// app.js
// NOTE: the `load` function is NOT a real implementation, it's just pseudo code.
load('ayncComponent.js').then(() => {
// `MyAsyncReducer` will be **merged** into the existed ones, not replace them
mirror.defaults({
reducers: {
MyAsyncReducer: (state, data) => {},
// ...
}
})
// do the re-render
mirror.render()
})
This is very useful for large apps.
Note: it's not recommended to pass
component
andcontainer
to re-render your app, because React may unmount/mount your app. If you just want to re-render, callrender
without any arguments.
Mirror uses react-router@4.x, so if you're from react-router 2.x/3.x, you should checkout the Migrating from v2/v3 to v4 Guide.
This is an enhanced Router
component from react-router. The history
and store
is automatically passed to Router
, all you have to do is declare your routes. But if you like, you can also create your own history
object and pass it as a prop to Router
component.
What about props like basename
or getUserConfirmation
? Well, Mirror's Router
handles them all! For a complete list of props Router
takes, check out BrowserRouter
, HashRouter
and MemoryRouter
.
The following components from react-router
are also exported by Mirror:
A simple example:
import {render, Router, Route, Link} from 'mirrorx'
// ...
const App = () => (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
</nav>
<div>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</div>
</div>
)
render(
<Router>
<App/>
</Router>
, document.getElementById('root'))
For more details, checkout the simple-router example, and react-router Docs.
Since
1.1.0
A method transforms all 'models' defined by mirror.model
to an object of standard Redux reducers, which can be directly handled by combineReducers
. In case you do not want the render
part of mirrorx, you can use toReducers
to get the reducer to create your own store by hand.
For example:
import { createStore, combineReducers } from 'redux'
import mirror, { actions } from 'mirrorx'
mirror.model({
initialState: 0,
name: 'count',
reducers: {
increment(state) {
return state + 1
},
decrement(state) {
return state - 1
},
add(state, data) {
return state + data
}
}
})
// `toReducers()` will generate an object whose keys are model names and values are corresponding reducers,
// then combine them by `combineReducers` will create a single reducer to be used to create the store.
const reducer = combineReducers(mirror.toReducers())
// create the store
const store = createStore(reducer)
store.getState()
// 0
store.dispatch({ type: 'count/increment' })
store.getState()
// 1
But, if you try to dispatch actions by actions.count.increment()
, an error will occur:
// ...
actions.count.increment()
// Error: You are calling "dispatch" or "getState" without applying mirrorMiddleware! Please create your store with mirrorMiddleware first!
In this case you'll have to apply the middleware provided by mirorrx to use actions
, see below for more details.
Since
1.1.0
A Redux middleware that makes actions
and effects
possible, it MUST be applied if you want both manually created store and the handy actions
:
import { createStore, applyMiddleware, combineReducers } from 'redux'
import mirror, { actions, middleware } from 'mirrorx'
mirror.model({
initialState: 0,
name: 'count',
reducers: {
add(state, data) {
return state + data
}
}
})
const reducer = combineReducers(mirror.toReducers())
// create the store with middleware applied
const store = createStore(reducer, applyMiddleware(middleware))
actions.count.add(10)
store.getState()
// 10