UNPKG

mirrorx

Version:

A React framework with minimal API and zero boilerplate.

765 lines (554 loc) 24.2 kB
# API * [mirror.model](#mirrormodelname-initialstate-reducers-effects) * [`name`](#-name) * [`initialState`](#-initialState) * [`reducers`](#-reducers) * [`effects`](#-effects) * [actions](#actions) * [mirror.hook](#mirrorhookaction-getstate--) * [mirror.defaults](#mirrordefaultsoptions) * [`initialState`](#-optionsinitialstate) * [`historyMode`](#-optionshistorymode) * [`middlewares`](#-optionsmiddlewares) * [`reducers`](#-optionsreducers) * [`addEffect`](#-optionsaddeffect) * [connect](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) * [render](#rendercomponent-container-callback) * [Router](#router) * [toReducers](#toreducers) * [middleware](#middleware) ### 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: ```js import mirror from 'mirrorx' mirror.model({ name: 'app', }) ``` Then you will get a `Redux` store like this: ```js // ... 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](https://github.com/ReactTraining/react-router/tree/master/packages/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: ```js import mirror from 'mirrorx' mirror.model({ name: 'app', + initialState: 0, }) ``` After store is created: ```js 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. ```js -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](http://redux.js.org/docs/basics/Reducers.html); 2. Create a [action type](http://redux.js.org/docs/basics/Actions.html) (`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: ```js // ... 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](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#oh-to-be-pure-again) too**), except one tiny difference: ```js // 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**. ```js // ... // 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`](http://redux.js.org/docs/api/combineReducers.html)), and then used to create your `Redux` store. > Note: non-function entries in `reducers` is pointless, and will be ignored(same as `effects`): > > ```js > import mirror, {actions} from 'mirrorx' > > mirror.model({ > name: 'app', > reducers: { > a: 1 > }, > }) > > actions.app // undefined > ``` #### * `effects` **`effects` are [async actions of Redux](http://redux.js.org/docs/advanced/AsyncActions.html)**. In functional programming, [`effect`](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md#side-effects-may-include) 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. ```js 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`: ```js // ... // 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](https://github.com/gaearon/redux-thunk) or [redux-saga](https://redux-saga.js.org). 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`: ```js // ... effects: { promisedEffect(data, getState) { return Promise.resolve(data).then(result => { // call your sync actions }) } } ``` Or, you can even go the old school `callback`(**discouraged**): ```js // ... 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`: > > ```js > 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 `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: ```js actions.app.add(1) ``` Is exactly the same as the following code: ```js 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: ```js // a.js import mirror, {actions} from 'mirrorx' mirror.model({ name: 'a', initialState: 0, reducers: { add(state, data) { return state + data }, }, }) ``` In b.js: ```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`](#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](https://github.com/ReactTraining/history/blob/v3/docs/GettingStarted.md#navigation). And thanks to [react-router-redux](https://github.com/reactjs/react-router-redux), an action will be dispatched when called, so your history will be synced with your store. ```js 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](https://github.com/ReactTraining/history/blob/v3/docs/Location.md#location). > Note: if your app does not use [`Router`](#router), `actions.routing` would be `undefined`. ### mirror.hook((action, getState) => {}) Add a hook to monitor actions that have been dispatched. ```js 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`](http://redux.js.org/docs/api/createStore.html) for your Mirror app's store. ```js mirror.defaults({ initialState: {app: 1} }) mirror.model({ name: 'app', // ... }) // ... store.getState() // {app: 1} ``` #### * `options.historyMode` * Default: `browser` The [history type](https://github.com/ReactTraining/history#usage) 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](https://github.com/ReactTraining/history) package. #### * `options.middlewares` * Default: `[]` Specifies a list of [Redux middleware](http://redux.js.org/docs/advanced/Middleware.html). This option is useful if you want to use some third party middlewares. In this case, you have to [`connect`](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 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`](http://redux.js.org/docs/api/combineReducers.html). For example, to use [redux-form](https://redux-form.com/), you can add redux-form's reducer as the following: ```js 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-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](#rendercomponent-container-callback), for example: ```js // 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](https://github.com/ShMcK/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`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) method from [react-redux](https://github.com/reactjs/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](http://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-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`](https://facebook.github.io/react/docs/react-dom.html#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](http://redux.js.org/docs/api/Store.html#replaceReducer) 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`: ```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: ```html <div> <div>0</div> <div></div> </div> ``` Then, suppose you have an async component/model which can be loaded by tools like [react-loadable](https://github.com/jamiebuilds/react-loadable): ```js // asyncComponent.js // inside this async component, you define an "async model" mirror.model({ name: 'bar', initialState: 'state of bar' }) ``` ```js // 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`: ```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: ```js // 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](https://github.com/ReactTraining/react-router), so if you're from react-router 2.x/3.x, you should checkout the [Migrating from v2/v3 to v4 Guide](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/migrating.md). This is an enhanced `Router` component from [react-router](https://github.com/ReactTraining/react-router/tree/master/packages/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`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md#basename-string) or [`getUserConfirmation`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md#getuserconfirmation-func)? Well, Mirror's `Router` handles them all! For a complete list of props `Router` takes, check out [`BrowserRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/BrowserRouter.md), [`HashRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/HashRouter.md) and [`MemoryRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/MemoryRouter.md). The following components from `react-router` are also exported by Mirror: * [`Route`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md) * [`Switch`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Switch.md) * [`Redirect`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Redirect.md) * [`Link`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/Link.md) * [`NavLink`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/NavLink.md) * [`Prompt`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Prompt.md) * [`withRouter`](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md) A simple example: ```js 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](https://github.com/mirrorjs/mirror/blob/master/examples/simple-router), and [react-router Docs](https://github.com/ReactTraining/react-router/tree/master/packages/react-router). ### toReducers() > Since `1.1.0` A method transforms all 'models' defined by [`mirror.model`](#mirrormodelname-initialstate-reducers-effects) to an object of standard Redux reducers, which can be directly handled by [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html). 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: ```js 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: ```js // ... 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`](#actions), see [below](#middleware) for more details. ### middleware > Since `1.1.0` A Redux middleware that makes [`actions`](#actions) and [`effects`](#-effects) possible, it MUST be applied if you want both manually created store and the handy `actions`: ```js 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 ```