Skip to content

Latest commit

 

History

History
582 lines (420 loc) · 17.6 KB

api.md

File metadata and controls

582 lines (420 loc) · 17.6 KB

API

mirror.model({name, initialState, reducers, effects})

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.

* name

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.

* initialState

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

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:

  1. Create a reducer;
  2. Create a action type (app/add in this case), which will be captured by the created reducer;
  3. Add a function whose name is the reducer's name under actions.<modelName> object. This function, when called, will dispatch 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 as effects):

import mirror, {actions} from 'mirrorx'

mirror.model({
  name: 'app',
  reducers: {
    a: 1
  },
})

actions.app // undefined

* effects

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 in actions.<modelName>.
  • getState - It's actually store.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 in reducers:

import mirror, {actions} from 'mirrorx'

// Will throw an error
mirror.model({
  name: 'app',
  reducers: {
    add(state, data) {
    }
  },
  effects: {
    add(data, getState) {
    }
  }
})

actions

The actions object contains both your Redux actions and reducers. 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)
    },
  },
})

* actions.routing

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 be undefined.

mirror.hook((action, getState) => {})

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(options)

mirror.defaults is a pretty intuitive API, you use it to configure your Mirror app.

* options.initialState

  • Default: undefined

The preloadedState for your Mirror app's store.

mirror.defaults({
  initialState: {app: 1}
})

mirror.model({
  name: 'app',
  // ...
})

// ...

store.getState()
// {app: 1}

* options.historyMode

  • 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.

* options.middlewares

  • 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([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

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 without mapDispatchToProps specified, then you'll get props.dispatch, which gives you the power to use some third party middlewares. This is the only case you should manually call dispatch in you component, in other cases, always use methods in actions to dispatch actions.

render([component], [container], [callback])

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.renderrender 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 and container to re-render your app, because React may unmount/mount your app. If you just want to re-render, call render without any arguments.

Router

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.