Skip to content

Latest commit

 

History

History
764 lines (554 loc) · 23.7 KB

api.md

File metadata and controls

764 lines (554 loc) · 23.7 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.resolve(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.

mirror.defaults can be called multiple times.

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

* options.reducers

  • 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
  }
})
Update, not replace

There's something special about options.reducers, that is its key-values 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.

* options.addEffect

  • Default: (effects) => (name, handler) => { effects[name] = handler }

Presents an option to configure how effects are created. Checkout mirror-saga for more information.

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, it's very convenient for code-splitting.

Update models on the fly

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>

Update standard reducers on the fly

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

toReducers()

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.

middleware

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