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.resole(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.
- 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.
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.
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, somewhere in you app, you define a model bar
, or load it from a remote server:
// ...
// inject an async model, this will not trigger the re-render
mirror.model({
name: 'bar',
initialState: 'state of bar'
})
// this will do the re-render
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>
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.
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.