├── .eslintignore
├── .npmignore
├── logo
└── logo.png
├── .gitignore
├── .travis.yml
├── __tests__
├── useStore.spec.js
├── helpers
│ ├── middlewares.js
│ ├── store.js
│ ├── reducers.js
│ └── components.js
├── createDispatch.spec.js
├── shallowEqual.spec.js
├── bindActionCreators.spec.js
├── combineReducers.spec.js
├── compose.spec.js
├── provider.spec.js
├── createStore.spec.js
└── connect.spec.js
├── src
├── utils
│ ├── isPlainObject.js
│ ├── objValueFunc.js
│ ├── compose.js
│ ├── shallowEqual.js
│ └── createDispatch.js
├── index.js
├── Provider.js
├── bindActionCreators.js
├── combineReducers.js
├── store.js
└── connect.js
├── .babelrc.js
├── .eslintrc
├── .editorconfig
├── rollup.config.js
├── LICENSE.md
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/build/**
2 | **/node_modules/**
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | __tests__
3 | build_config
4 | .log
5 | package-lock.json
6 |
--------------------------------------------------------------------------------
/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iusehooks/redhooks/HEAD/logo/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .log
3 | .git
4 | .DS_Store
5 | dist
6 | build
7 | coverage
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | install:
5 | - npm i -g npm@6.5.0
6 | - npm ci
7 | branches:
8 | only:
9 | - master
10 | cache: npm
11 | after_success:
12 | - npm run coveralls
13 |
--------------------------------------------------------------------------------
/__tests__/useStore.spec.js:
--------------------------------------------------------------------------------
1 | import { useStore } from "../src/store";
2 |
3 | describe("useStore", () => {
4 | it("should throw if not called within a function component", () => {
5 | expect(() => useStore()).toThrow();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/utils/isPlainObject.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {Object} obj A plain Object.
3 | *
4 | * @returns {Boolean} It returns true the argument passed is plain object.
5 | */
6 |
7 | export default obj => Object.prototype.toString.call(obj) === "[object Object]";
8 |
--------------------------------------------------------------------------------
/src/utils/objValueFunc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {Object} obj A plain Object.
3 | *
4 | * @returns {Boolean} It returns true if all values of the object are functions.
5 | */
6 |
7 | export default obj =>
8 | Object.keys(obj).every(key => typeof obj[key] === "function");
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as combineReducers } from "./combineReducers";
2 | export { createStore, useStore } from "./store";
3 | export { default as connect } from "./connect";
4 | export { default as bindActionCreators } from "./bindActionCreators";
5 | export { default } from "./Provider";
6 |
--------------------------------------------------------------------------------
/__tests__/helpers/middlewares.js:
--------------------------------------------------------------------------------
1 | const logStateAfterAction = state => state; // log state
2 |
3 | export const logger = ({ getState }) => next => action => {
4 | const value = next(action);
5 | logStateAfterAction(getState());
6 | return value;
7 | };
8 | export const thunk = ({ dispatch, getState }) => next => action => {
9 | if (typeof action === "function") {
10 | return action(dispatch, getState);
11 | }
12 | return next(action);
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/compose.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Composes single-argument functions from right to left.
3 | *
4 | * @param {...Function} funcs The functions to compose.
5 | * @returns {Function} A function obtained by composing the argument functions
6 | * from right to left.
7 | */
8 |
9 | export default (...funcs) =>
10 | funcs.length === 0
11 | ? arg => arg
12 | : (...args) =>
13 | funcs.reduceRight(
14 | (acc, elm, index) => (index < funcs.length - 1 ? elm(acc) : acc),
15 | funcs[funcs.length - 1](...args)
16 | );
17 |
--------------------------------------------------------------------------------
/src/utils/shallowEqual.js:
--------------------------------------------------------------------------------
1 | export default function shallowEqual(objA, objB) {
2 | if (
3 | typeof objA !== "object" ||
4 | objA === null ||
5 | typeof objB !== "object" ||
6 | objB === null
7 | ) {
8 | return false;
9 | }
10 |
11 | const keysA = Object.keys(objA);
12 | const keysB = Object.keys(objB);
13 |
14 | if (keysA.length !== keysB.length) return false;
15 |
16 | for (let i = 0; i < keysA.length; i++) {
17 | if (objA[keysA[i]] !== objB[keysA[i]]) {
18 | return false;
19 | }
20 | }
21 | return true;
22 | }
23 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | let presets = [
2 | [
3 | "@babel/preset-env",
4 | {
5 | loose: true,
6 | modules: false,
7 | exclude: ["transform-async-to-generator", "transform-regenerator"],
8 | targets: {
9 | browsers: ["ie >= 11"]
10 | }
11 | }
12 | ],
13 | "@babel/preset-react"
14 | ];
15 |
16 | let plugins = ["@babel/plugin-proposal-class-properties"];
17 | if (process.env.NODE_ENV === "test") {
18 | plugins.push("@babel/transform-modules-commonjs");
19 | }
20 |
21 | module.exports = {
22 | presets,
23 | plugins
24 | };
25 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:flowtype/recommended",
4 | "plugin:react/recommended",
5 | "prettier/react",
6 | "prettier/standard",
7 | "prettier/flowtype"
8 | ],
9 | "plugins": [
10 | "prettier",
11 | "react",
12 | "flowtype"
13 | ],
14 | "parserOptions": {
15 | "ecmaVersion": 2017,
16 | "sourceType": "module",
17 | "ecmaFeatures": {
18 | "jsx": true
19 | }
20 | },
21 | "rules": {
22 | "prettier/prettier": "error",
23 | "react/prop-types": 0,
24 | "react/display-name": 0
25 | },
26 | "env": {
27 | "es6": true,
28 | "browser": true,
29 | "node": true,
30 | "jest": true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/__tests__/helpers/store.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, createStore } from "./../../src";
2 | import { counterReducer, todoReducer } from "./reducers";
3 |
4 | let state;
5 | export const fakeStore = (reducer, initialState) => {
6 | state = initialState;
7 | return {
8 | getState: () => state,
9 | dispatch: action => {
10 | const nextState = reducer(state, action);
11 | state = nextState;
12 | return action.type;
13 | }
14 | };
15 | };
16 |
17 | export const store = createStore(
18 | combineReducers({ counterReducer, todoReducer })
19 | );
20 |
21 | export const storeWithMiddlewares = middlewares =>
22 | createStore(combineReducers({ counterReducer, todoReducer }), {
23 | middlewares
24 | });
25 |
--------------------------------------------------------------------------------
/src/utils/createDispatch.js:
--------------------------------------------------------------------------------
1 | import compose from "./compose";
2 |
3 | /**
4 | * It applies middleware, if any are passed, and creates
5 | * the dispatcher of the Redhooks store.
6 | *
7 | * @param {...Function} middlewares The middleware chain to be applied.
8 | * @returns {Function}
9 | */
10 |
11 | export default (...middlewares) => store => {
12 | let dispatch = () => {
13 | throw new Error(`Dispatcher not ready yet!`);
14 | };
15 |
16 | const middlwareAPI = {
17 | getState: store.getState,
18 | dispatch: (...args) => dispatch(...args)
19 | };
20 | const chain = middlewares.map(middleware => middleware(middlwareAPI));
21 | dispatch = compose(...chain)(store.dispatch);
22 |
23 | return dispatch;
24 | };
25 |
--------------------------------------------------------------------------------
/__tests__/helpers/reducers.js:
--------------------------------------------------------------------------------
1 | export const counterReducer = (state = 0, { type }) => {
2 | switch (type) {
3 | case "INCREMENT":
4 | return state + 1;
5 | case "DECREMENT":
6 | return state - 1;
7 | default:
8 | return state;
9 | }
10 | };
11 |
12 | export const todoReducer = (state = [], { type, payload }) => {
13 | switch (type) {
14 | case "ADD_TODO":
15 | return [...state, payload];
16 | default:
17 | return state;
18 | }
19 | };
20 |
21 | export const genericReducer = (state = { a: 1 }, { type, payload }) => {
22 | switch (type) {
23 | case "DO_SOMETHING":
24 | return { ...state, payload };
25 | default:
26 | return state;
27 | }
28 | };
29 |
30 | export const plainReducersObj = {
31 | genericReducer,
32 | todoReducer,
33 | counterReducer
34 | };
35 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | [*]
8 | # Indentation style
9 | # Possible values - tab, space
10 | indent_style = space
11 |
12 | # Indentation size in single-spaced characters
13 | # Possible values - an integer, tab
14 | indent_size = 2
15 |
16 | # Line ending file format
17 | # Possible values - lf, crlf, cr
18 | end_of_line = lf
19 |
20 | # File character encoding
21 | # Possible values - latin1, utf-8, utf-16be, utf-16le
22 | charset = utf-8
23 |
24 | # Denotes whether to trim whitespace at the end of lines
25 | # Possible values - true, false
26 | trim_trailing_whitespace = false
27 |
28 | # Denotes whether file should end with a newline
29 | # Possible values - true, false
30 | insert_final_newline = true
31 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import { terser } from "rollup-plugin-terser";
3 | import replace from "rollup-plugin-replace";
4 | import commonjs from "rollup-plugin-commonjs";
5 | import nodeResolve from "rollup-plugin-node-resolve";
6 |
7 | const config = {
8 | input: "./src/index.js",
9 | output: {
10 | name: "Redhooks",
11 | globals: {
12 | react: "React"
13 | },
14 | exports: "named"
15 | },
16 | external: ["react"],
17 | plugins: [
18 | babel({
19 | exclude: "node_modules/**"
20 | }),
21 | nodeResolve(),
22 | commonjs({
23 | include: /node_modules/
24 | }),
25 | replace({
26 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
27 | })
28 | ]
29 | };
30 |
31 | if (process.env.NODE_ENV === "production") {
32 | config.plugins.push(
33 | terser({
34 | compress: {
35 | pure_getters: true,
36 | unsafe: true,
37 | unsafe_comps: true,
38 | warnings: false
39 | }
40 | })
41 | );
42 | }
43 |
44 | export default config;
45 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright © 2019-present Antonio Pangallo
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/Provider.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import createDispatch from "./utils/createDispatch";
3 | import { Context } from "./store";
4 |
5 | export default function Provider({ store, children }) {
6 | const { reducer, initialState, middlewares, onload } = store;
7 |
8 | const storeContext = storeHooks(reducer, initialState, middlewares);
9 |
10 | useEffect(() => {
11 | onload && onload(storeContext);
12 | }, []);
13 |
14 | return {children};
15 | }
16 |
17 | function storeHooks(reducer, initialState, middlewares) {
18 | const [state, setState] = useState(() => initialState);
19 |
20 | const stateProvider = useRef();
21 | stateProvider.current = state; // store the state reference
22 |
23 | const [dispatch] = useState(() => {
24 | const storeAPI = {
25 | getState: () => stateProvider.current,
26 | dispatch: action => {
27 | const nextState = reducer(stateProvider.current, action);
28 | if (nextState !== stateProvider.current) {
29 | stateProvider.current = nextState; // update the state reference
30 | setState(nextState);
31 | }
32 | return action;
33 | }
34 | };
35 | return createDispatch(...middlewares)(storeAPI);
36 | });
37 |
38 | return { state, dispatch };
39 | }
40 |
--------------------------------------------------------------------------------
/__tests__/createDispatch.spec.js:
--------------------------------------------------------------------------------
1 | import createDispatch from "../src/utils/createDispatch";
2 | import { logger, thunk } from "./helpers/middlewares";
3 | import { fakeStore } from "./helpers/store";
4 | import { counterReducer } from "./helpers/reducers";
5 |
6 | const initialState = 0;
7 | let store;
8 |
9 | describe("Utils => createDispatch", () => {
10 | beforeEach(() => (store = fakeStore(counterReducer, initialState)));
11 |
12 | it("should create the store dispatcher given middlewares functions as args", () => {
13 | const disptach = createDispatch(logger, thunk)(store);
14 | const action = disptach({ type: "init" });
15 | expect(action).toEqual("init");
16 | });
17 |
18 | it("should dispatch an action to change the state", () => {
19 | const disptach = createDispatch(logger, thunk)(store);
20 | disptach({ type: "INCREMENT" });
21 | expect(store.getState()).toBe(1);
22 | disptach({ type: "DECREMENT" });
23 | expect(store.getState()).toBe(0);
24 | });
25 |
26 | it("should dispatch a function handled by thunk middleware", () => {
27 | const disptach = createDispatch(logger, thunk)(store);
28 | const changeCounter = action => disptach => disptach(action);
29 | disptach(changeCounter({ type: "DECREMENT" }));
30 | expect(store.getState()).toBe(-1);
31 | });
32 |
33 | it("should throw when dispatcher is not ready yet", () => {
34 | function dispatchBeforeIsReady(store) {
35 | store.dispatch({ type: "DO_SOMETHING" });
36 | return next => action => next(action);
37 | }
38 | expect(() => createDispatch(dispatchBeforeIsReady)(store)).toThrow();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/bindActionCreators.js:
--------------------------------------------------------------------------------
1 | import isPlainObject from "./utils/isPlainObject";
2 |
3 | /**
4 | * Turns an object whose values are action creator, into an object with the
5 | * same keys, but with every function wrapped into a `dispatch` call so they
6 | * may be invoked directly.
7 | *
8 | * @param {Object} actionCreators An object whose values are action
9 | * creator functions or plain objects whose values are action creator functions
10 | *
11 | * @param {Function} dispatch The `dispatch` function available on your Redhooks
12 | * store.
13 | *
14 | * @returns {Object} if passed may be either a function that must return a plain object
15 | * whose values are functions or a plain object whose values are action creator functions
16 | */
17 |
18 | const bindActionCreators = (actionCreators, dispatch) => {
19 | if (!isPlainObject(actionCreators)) {
20 | throw new Error(errMsg("First", "must be a plain object", actionCreators));
21 | }
22 |
23 | if (typeof dispatch !== "function") {
24 | throw new Error(errMsg("Second", "must be a function", dispatch));
25 | }
26 |
27 | return Object.keys(actionCreators).reduce((acc, key) => {
28 | const value = actionCreators[key];
29 | if (typeof value === "function") {
30 | return { ...acc, [key]: (...args) => dispatch(value(...args)) };
31 | } else if (isPlainObject(value)) {
32 | return { ...acc, [key]: { ...bindActionCreators(value, dispatch) } };
33 | }
34 | return acc;
35 | }, {});
36 | };
37 |
38 | export default bindActionCreators;
39 |
40 | function errMsg(argNumber, text, prop) {
41 | return `${argNumber} argument in bindActionCreators() ${text}. Instead received ${typeof prop} => ${prop}.`;
42 | }
43 |
--------------------------------------------------------------------------------
/__tests__/shallowEqual.spec.js:
--------------------------------------------------------------------------------
1 | import shallowEqual from "../src/utils/shallowEqual";
2 |
3 | /* Unit Tests Taken from */
4 | /* https://github.com/moroshko/shallow-equal/blob/master/src/objects.test.js */
5 |
6 | const obj1 = { game: "chess", year: "1979" };
7 | const obj2 = { language: "elm" };
8 |
9 | const tests = [
10 | {
11 | should: "return false when A is falsy",
12 | objA: null,
13 | objB: {},
14 | result: false
15 | },
16 | {
17 | should: "return false when B is falsy",
18 | objA: {},
19 | objB: undefined,
20 | result: false
21 | },
22 | {
23 | should: "return true when objects are ===",
24 | objA: obj1,
25 | objB: obj1,
26 | result: true
27 | },
28 | {
29 | should: "return true when both objects are empty",
30 | objA: {},
31 | objB: {},
32 | result: true
33 | },
34 | {
35 | should: "return false when objects do not have the same amount of keys",
36 | objA: { game: "chess", year: "1979", country: "Australia" },
37 | objB: { game: "chess", year: "1979" },
38 | result: false
39 | },
40 | {
41 | should: "return false when there corresponding values which are not ===",
42 | objA: { first: obj1, second: obj2 },
43 | objB: { first: obj1, second: { language: "elm" } },
44 | result: false
45 | },
46 | {
47 | should: "return true when all values are ===",
48 | objA: { first: obj1, second: obj2 },
49 | objB: { second: obj2, first: obj1 },
50 | result: true
51 | },
52 | {
53 | should: "return false when one of the input args is not a object",
54 | objA: 3,
55 | objB: {},
56 | result: false
57 | }
58 | ];
59 |
60 | describe("shallowEqualObjects", function() {
61 | tests.forEach(function(test) {
62 | it("should " + test.should, function() {
63 | expect(shallowEqual(test.objA, test.objB)).toBe(test.result);
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/__tests__/helpers/components.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect, useStore } from "./../../src";
3 |
4 | export const Increment = () => {
5 | const { state, dispatch } = useStore();
6 | const increment = () => dispatch({ type: "INCREMENT" });
7 | return ;
8 | };
9 |
10 | class IncrementConnected extends React.Component {
11 | constructor(props) {
12 | super(props);
13 | const { dispatch } = props;
14 | this._increment = () => dispatch({ type: "INCREMENT" });
15 | }
16 | render() {
17 | const { counterReducer } = this.props;
18 | return ;
19 | }
20 | }
21 |
22 | function mapStateToProps(state) {
23 | return {
24 | counterReducer: state.counterReducer
25 | };
26 | }
27 |
28 | export const IncrementClassCPM = connect(mapStateToProps)(IncrementConnected);
29 |
30 | const ConnectedOwnPropChange = connect()(IncrementConnected);
31 | export class WrapperChangeOwnProp extends React.Component {
32 | constructor(props) {
33 | super(props);
34 | this.state = { counterReducer: 0 };
35 | }
36 | updateCounter(counterReducer) {
37 | this.setState({ counterReducer });
38 | }
39 | render() {
40 | const { counterReducer } = this.state;
41 | return ;
42 | }
43 | }
44 |
45 | export const BasicComponent = props => (
46 |
49 | );
50 |
51 | export const BasicComponentNoMapDispatch = props => (
52 |
55 | );
56 |
57 | export const BasicComponentEmptyAction = () => {
58 | const { state, dispatch } = useStore();
59 | return ;
60 | };
61 |
--------------------------------------------------------------------------------
/__tests__/bindActionCreators.spec.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from "../src";
2 |
3 | const actionCreators = {
4 | increment: () => ({ type: "INCREMENT" }),
5 | actions: {
6 | decrement: () => ({ type: "DECREMENT" })
7 | },
8 | nested: {
9 | a: () => ({ type: "DECREMENT" }),
10 | inner: {
11 | b: () => ({ type: "DECREMENT" }),
12 | c: () => ({ type: "DECREMENT" })
13 | }
14 | }
15 | };
16 | const dispatch = action => action;
17 | const actionCreatorFunctions = { ...actionCreators };
18 |
19 | describe("bindActionCreators", () => {
20 | it("should wrap the action creators with a given function", () => {
21 | let boundActionCreators = bindActionCreators(actionCreators, dispatch);
22 | expect(Object.keys(actionCreators)).toEqual(
23 | Object.keys(actionCreatorFunctions)
24 | );
25 |
26 | let action = boundActionCreators.increment();
27 | expect(action).toEqual(actionCreators.increment());
28 |
29 | action = boundActionCreators.actions.decrement();
30 | expect(action).toEqual(actionCreators.actions.decrement());
31 |
32 | action = boundActionCreators.nested.a();
33 | expect(action).toEqual(actionCreators.nested.a());
34 |
35 | action = boundActionCreators.nested.inner.b();
36 | expect(action).toEqual(actionCreators.nested.inner.b());
37 | });
38 |
39 | it("should throw if invalid arguments are passed", () => {
40 | expect(() => bindActionCreators(1, dispatch)).toThrow();
41 | expect(() => bindActionCreators(false, dispatch)).toThrow();
42 | expect(() => bindActionCreators("test", dispatch)).toThrow();
43 | expect(() => bindActionCreators(null, dispatch)).toThrow();
44 | expect(() => bindActionCreators(undefined, dispatch)).toThrow();
45 |
46 | expect(() => bindActionCreators(actionCreators, 1)).toThrow();
47 | expect(() => bindActionCreators(actionCreators, false)).toThrow();
48 | expect(() => bindActionCreators(actionCreators, "test")).toThrow();
49 | expect(() => bindActionCreators(actionCreators, null)).toThrow();
50 | expect(() => bindActionCreators(actionCreators, undefined)).toThrow();
51 | expect(() => bindActionCreators(actionCreators, {})).toThrow();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/__tests__/combineReducers.spec.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, createStore } from "./../src";
2 | import { counterReducer, todoReducer } from "./helpers/reducers";
3 |
4 | describe("combineReducers", () => {
5 | it("should combine reducers funtions in one", () => {
6 | const rootReducer = combineReducers({ counterReducer, todoReducer });
7 | const { initialState } = createStore(rootReducer);
8 | let newState = rootReducer(initialState, { type: "INCREMENT" });
9 | expect(newState.counterReducer).toBe(1);
10 | newState = rootReducer(newState, { type: "DECREMENT" });
11 | expect(newState.counterReducer).toBe(0);
12 | newState = rootReducer(newState, { type: "NONE" });
13 | expect(newState.counterReducer).toBe(0);
14 | });
15 |
16 | it("should throw if a reducer return undefined", () => {
17 | const undefinedRed = () => undefined;
18 | const rootReducer = combineReducers({ undefinedRed });
19 | expect(() => rootReducer(0, { type: "DECREMENT" })).toThrow();
20 | expect(() => rootReducer(0, undefined)).toThrow();
21 | expect(() => rootReducer(0, {})).toThrow();
22 | expect(() => rootReducer(0)).toThrow();
23 | });
24 |
25 | it("should throw if an invalid argument is passed", () => {
26 | expect(() => combineReducers(null)).toThrow();
27 | expect(() => combineReducers(undefined)).toThrow();
28 | expect(() => combineReducers(false)).toThrow();
29 | expect(() => combineReducers("hello")).toThrow();
30 | expect(() => combineReducers(1)).toThrow();
31 | expect(() => combineReducers(todoReducer)).toThrow();
32 | expect(() => combineReducers({})).toThrow();
33 | });
34 |
35 | it("should throw if an invalid value for the argument is passed", () => {
36 | let wrongRed = false;
37 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
38 | wrongRed = 3;
39 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
40 | wrongRed = "hello";
41 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
42 | wrongRed = {};
43 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
44 | wrongRed = { wrongRed };
45 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
46 | wrongRed = null;
47 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
48 | wrongRed = undefined;
49 | expect(() => combineReducers({ counterReducer, wrongRed })).toThrow();
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redhooks",
3 | "version": "1.1.0",
4 | "description": "Predictable state container for React apps written using Hooks.",
5 | "main": "./build/index.js",
6 | "module": "./build/es/index.js",
7 | "repository": "github:iusehooks/redhooks",
8 | "scripts": {
9 | "build": "npm run clean && node ./build_config/build.js",
10 | "clean": "rimraf build coverage",
11 | "prepare": "npm run clean && npm run lint && npm run test && npm run build",
12 | "lint": "eslint --ext .jsx --ext .js src __tests__ --fix",
13 | "pretest": "npm run lint",
14 | "test": "cross-env NODE_ENV=test jest -u",
15 | "test:watch": "npm run test -- --watch",
16 | "test:cov": "npm test -- --coverage",
17 | "coveralls": "npm run test:cov && cat ./coverage/lcov.info | coveralls"
18 | },
19 | "author": "Antonio Pangallo (https://github.com/antoniopangallo)",
20 | "files": [
21 | "build/index.js",
22 | "build/index.es.js",
23 | "build/es",
24 | "build/umd"
25 | ],
26 | "keywords": [
27 | "redhooks",
28 | "reducer",
29 | "state",
30 | "predictable",
31 | "react-hooks",
32 | "hooks"
33 | ],
34 | "license": "MIT",
35 | "peerDependencies": {
36 | "react": ">=16.8.0"
37 | },
38 | "devDependencies": {
39 | "@babel/cli": "^7.2.3",
40 | "@babel/core": "^7.2.2",
41 | "@babel/plugin-proposal-class-properties": "^7.3.0",
42 | "@babel/preset-env": "^7.2.3",
43 | "@babel/preset-react": "^7.0.0",
44 | "babel-core": "^7.0.0-bridge.0",
45 | "babel-eslint": "^10.0.1",
46 | "child_process": "^1.0.2",
47 | "coveralls": "^3.0.2",
48 | "cross-env": "^5.2.0",
49 | "eslint": "^5.11.1",
50 | "eslint-config-prettier": "^3.3.0",
51 | "eslint-plugin-flowtype": "^3.2.0",
52 | "eslint-plugin-import": "^2.8.0",
53 | "eslint-plugin-prettier": "^3.0.1",
54 | "eslint-plugin-react": "^7.12.3",
55 | "jest": "^23.6.0",
56 | "prettier": "^1.9.2",
57 | "react": "16.8.0-alpha.0",
58 | "react-dom": "^16.8.0-alpha.0",
59 | "react-testing-library": "^5.4.4",
60 | "rimraf": "^2.6.3",
61 | "rollup": "^1.0.1",
62 | "rollup-plugin-babel": "^4.2.0",
63 | "rollup-plugin-commonjs": "^9.2.0",
64 | "rollup-plugin-node-resolve": "^4.0.0",
65 | "rollup-plugin-replace": "^2.0.0",
66 | "rollup-plugin-terser": "^4.0.1"
67 | },
68 | "jest": {
69 | "testRegex": "(/__tests__/.*\\.spec\\.js)$",
70 | "collectCoverageFrom": [
71 | "src/**/*.js"
72 | ]
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/__tests__/compose.spec.js:
--------------------------------------------------------------------------------
1 | /* https://github.com/reduxjs/redux/blob/master/test/compose.spec.js */
2 |
3 | import compose from "./../src/utils/compose";
4 |
5 | describe("Utils => compose", () => {
6 | it("composes functions from right to left", () => {
7 | const double = x => x * 2;
8 | const square = x => x * x;
9 | expect(compose(square)(5)).toBe(25);
10 | expect(
11 | compose(
12 | square,
13 | double
14 | )(5)
15 | ).toBe(100);
16 | expect(
17 | compose(
18 | double,
19 | square,
20 | double
21 | )(5)
22 | ).toBe(200);
23 | });
24 |
25 | it("chain functions from right to left", () => {
26 | const a = next => x => next(x + "a");
27 | const b = next => x => next(x + "b");
28 | const c = next => x => next(x + "c");
29 | const final = x => x;
30 |
31 | expect(
32 | compose(
33 | a,
34 | b,
35 | c
36 | )(final)("")
37 | ).toBe("abc");
38 | expect(
39 | compose(
40 | b,
41 | c,
42 | a
43 | )(final)("")
44 | ).toBe("bca");
45 | expect(
46 | compose(
47 | c,
48 | a,
49 | b
50 | )(final)("")
51 | ).toBe("cab");
52 | });
53 |
54 | it("throws at runtime if argument is not a function", () => {
55 | const square = x => x * x;
56 | const add = (x, y) => x + y;
57 |
58 | expect(() =>
59 | compose(
60 | square,
61 | add,
62 | false
63 | )(1, 2)
64 | ).toThrow();
65 | expect(() =>
66 | compose(
67 | square,
68 | add,
69 | undefined
70 | )(1, 2)
71 | ).toThrow();
72 | expect(() =>
73 | compose(
74 | square,
75 | add,
76 | true
77 | )(1, 2)
78 | ).toThrow();
79 | expect(() =>
80 | compose(
81 | square,
82 | add,
83 | NaN
84 | )(1, 2)
85 | ).toThrow();
86 | expect(() =>
87 | compose(
88 | square,
89 | add,
90 | "42"
91 | )(1, 2)
92 | ).toThrow();
93 | });
94 |
95 | it("can be seeded with multiple arguments", () => {
96 | const square = x => x * x;
97 | const add = (x, y) => x + y;
98 | expect(
99 | compose(
100 | square,
101 | add
102 | )(1, 2)
103 | ).toBe(9);
104 | });
105 |
106 | it("returns the first given argument if given no functions", () => {
107 | expect(compose()(1, 2)).toBe(1);
108 | expect(compose()(3)).toBe(3);
109 | expect(compose()()).toBe(undefined);
110 | });
111 | });
112 |
--------------------------------------------------------------------------------
/__tests__/provider.spec.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, fireEvent } from "react-testing-library";
3 |
4 | import Provider from "./../src";
5 | import { store, storeWithMiddlewares } from "./helpers/store";
6 | import {
7 | Increment,
8 | IncrementClassCPM,
9 | BasicComponentEmptyAction
10 | } from "./helpers/components";
11 |
12 | const mountProvider = ({ props, children } = {}) =>
13 | render({children});
14 |
15 | describe("Component => Provider", () => {
16 | it("should render a Provider and its children", () => {
17 | const children = [];
18 | const props = { store };
19 | const { container } = mountProvider({ props, children });
20 | expect(container.children.length).toBe(children.length);
21 | });
22 |
23 | it("should load the store after Provider is ready", done => {
24 | const props = { store };
25 | store.onload = jest.fn();
26 | mountProvider({ props });
27 | setTimeout(() => {
28 | expect(store.onload).toBeCalledTimes(1);
29 | done();
30 | });
31 | });
32 |
33 | it("should a Provider's function component child be able to dispatch actions and update the state", () => {
34 | const children = [];
35 | const props = { store, children };
36 | const { container } = mountProvider({ props, children });
37 | const button = container.firstChild;
38 | expect(button.textContent).toBe("0");
39 | fireEvent.click(button);
40 | expect(button.textContent).toBe("1");
41 | });
42 |
43 | it("should a Provider's class component child be able to dispatch actions and update the state", () => {
44 | const children = [];
45 | const props = { store, children };
46 | const { container } = mountProvider({ props, children });
47 | const button = container.firstChild;
48 | expect(button.textContent).toBe("0");
49 | fireEvent.click(button);
50 | expect(button.textContent).toBe("1");
51 | });
52 |
53 | it("should state not change when an empty action is dispatched", () => {
54 | const children = [];
55 | const props = { store, children };
56 | const { container } = mountProvider({ props, children });
57 | const button = container.firstChild;
58 | fireEvent.click(button);
59 | expect(button.textContent).toBe("0");
60 | });
61 |
62 | it("should a dispatched action go through the middlewares", done => {
63 | const middlware = ({ getState }) => next => action => {
64 | const value = next(action);
65 | const newState = getState();
66 | expect(newState.counterReducer).toBe(1);
67 | expect(value.type).toBe("INCREMENT");
68 | done();
69 | };
70 | const store = storeWithMiddlewares([middlware]);
71 | const children = [];
72 | const props = { store, children };
73 | const { container } = mountProvider({ props, children });
74 | const button = container.firstChild;
75 | fireEvent.click(button);
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/combineReducers.js:
--------------------------------------------------------------------------------
1 | import isPlainObject from "./utils/isPlainObject";
2 |
3 | /**
4 | * It combines an object whose props are different reducer functions, into a single
5 | * reducer function. It will call every child reducer, and gather their results
6 | * into a single state object, whose keys correspond to the keys of the passed
7 | * reducer functions.
8 | *
9 | * @param {Object} reducers An object whose values correspond to different
10 | * reducer functions that need to be combined into one. The reducers must never return
11 | * undefined for any action or being initilized as undefined.
12 | * Any unrecognized action must return the current state.
13 | *
14 | * @returns {Function} A reducer function that invokes every reducer inside the
15 | * passed object, and builds a state object with the same shape.
16 | */
17 |
18 | export default function(reducers = {}) {
19 | if (!isPlainObject(reducers)) {
20 | throw new Error(
21 | "The type passed as argument is not valid. It accepts object which values are functions"
22 | );
23 | }
24 | const reducersKeys = Object.keys(reducers);
25 | const validReducers = reducersKeys.filter(
26 | rKey => typeof reducers[rKey] === "function"
27 | );
28 |
29 | const errorMessage = areValidReducers(validReducers, reducersKeys);
30 | if (errorMessage) {
31 | throw new Error(errorMessage);
32 | }
33 |
34 | return function(state = {}, action) {
35 | let hasChanged = false;
36 | const nextState = {};
37 | validReducers.forEach(key => {
38 | const reducer = reducers[key];
39 | const previousStateForKey = state[key];
40 | const nextStateForKey = reducer(previousStateForKey, action);
41 | if (typeof nextStateForKey === "undefined") {
42 | const errorMessage = invalidStateErrorMessage(key, action);
43 | throw new Error(errorMessage);
44 | }
45 | nextState[key] = nextStateForKey;
46 |
47 | hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
48 | });
49 |
50 | return hasChanged ? { ...state, ...nextState } : state;
51 | };
52 | }
53 |
54 | function invalidStateErrorMessage(key, action) {
55 | const actionType = action && action.type;
56 | const commonText =
57 | "If you want this reducer to hold no value, you can return null instead of undefined.";
58 | if (!actionType) {
59 | return (
60 | `Reducer "${key}" returned undefined at initialization time or by passing an undefined action.` +
61 | `${commonText}`
62 | );
63 | } else {
64 | const actionDescription = `action "${String(actionType)}"`;
65 |
66 | return (
67 | `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
68 | `To ignore an action, you must explicitly return the previous state. ` +
69 | `${commonText}`
70 | );
71 | }
72 | }
73 |
74 | function areValidReducers(validReducers, reducersKeys) {
75 | if (validReducers.length === 0) {
76 | return "The object passed as argument does not have any valid reducer functions";
77 | }
78 |
79 | if (validReducers.length !== reducersKeys.length) {
80 | const invalidReducers = reducersKeys.filter(
81 | key => validReducers.indexOf(key) === -1
82 | );
83 | return `The object passed as argument contains invalid reducer functions for the keys: [${invalidReducers}]`;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 | import combineReducers from "./combineReducers";
3 | import isPlainObject from "./utils/isPlainObject";
4 | import objValueFunc from "./utils/objValueFunc";
5 |
6 | export const Context = createContext();
7 |
8 | /**
9 | * Creates a Redhooks store that holds the state tree.
10 | * The only way to change the data in the store is to call the `dispatch` method.
11 | * To do so you have to call `useStore()` from a function Component which returns an object { state, dispatch }.
12 | * To use the store from a class Component you have to use the `connect` HOC => export default connect(YourClassComponent)
13 | *
14 | * There should only be a single store in your app. To specify how different
15 | * parts of the state tree respond to actions, you may combine several reducers
16 | * into a single reducer function by using `combineReducers`.
17 | *
18 | * @param {Function|Object} reducer A function that given an action and the current state it returns the next state tree.
19 | * In case of multiple reducers this function may be created either by using `combineReducers` or by passing a plain object
20 | * whose values are reducer functions.
21 | * @param {Object} opts You may optionally specify a preloadedState and/or dispatch an initilaAction and/or middlewares
22 | * @param {Object} opts.preloadedState - To initialize the store with a State.
23 | * @param {Object} opts.initilaAction - To dispatch an initialAction to the Store => { type: "INIT", payload: "Hello" }.
24 | * @param {Array} opts.middlewares - An array of functions conforming to the Redhooks middleware.
25 | *
26 | * @returns {Store} A Redhooks store which has to be passed as prop to the
27 | */
28 |
29 | export const createStore = (reducer, opts = {}) => {
30 | if (isPlainObject(reducer)) {
31 | reducer = combineReducers(reducer);
32 | }
33 |
34 | if (typeof reducer !== "function") {
35 | throw new Error("The reducer must be a function");
36 | }
37 |
38 | if (!isPlainObject(opts)) {
39 | throw new Error(
40 | "Argument opts invalid in createStore(reducer, [opts]). It must be a object { preloadedState, initialAction, middlewares }"
41 | );
42 | }
43 |
44 | const { preloadedState, initialAction = {}, middlewares = [] } = opts;
45 |
46 | if (typeof preloadedState === "function") {
47 | throw new Error(
48 | "You are passing a function as preloadedState, it only accepts plain objects and primitives data types"
49 | );
50 | }
51 |
52 | if (!isPlainObject(initialAction)) {
53 | throw new Error(
54 | "Invalid initialAction, an action has to be a plain object like { type: 'ADD', payload: 1 }"
55 | );
56 | }
57 |
58 | if (middlewares !== null && middlewares.constructor === Array) {
59 | if (!objValueFunc(middlewares)) {
60 | throw new Error(
61 | "You are passing an invalid middleware. A middleware must be a function"
62 | );
63 | }
64 | } else {
65 | throw new Error(
66 | "The middlewares value of opts must be an array of functions"
67 | );
68 | }
69 |
70 | // At store creation time an empty action, if initilAction is not passed, is dispatched so that every
71 | // reducer returns their initial state to populate the initial state tree.
72 | let initialState = reducer(preloadedState, initialAction);
73 |
74 | return { initialState, reducer, middlewares };
75 | };
76 |
77 | /**
78 | You can call `useStore()` only within a function Component. It returns an object { state, dispatch }.
79 | To use the store from a class Component you have to use the `connect` HOC.
80 | export default connect([mapStateToProps], [mapDispatchToProps])(YourClassComponent)
81 | */
82 |
83 | export const useStore = () => useContext(Context);
84 |
--------------------------------------------------------------------------------
/__tests__/createStore.spec.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "../src/store";
2 | import { counterReducer, plainReducersObj } from "./helpers/reducers";
3 |
4 | describe("createStore", () => {
5 | it("should create a store with the given reducer function or plain object", () => {
6 | let store = createStore(counterReducer);
7 | expect(store.initialState).toBe(0);
8 | store = createStore(plainReducersObj);
9 | expect(store.initialState.counterReducer).toBe(0);
10 | expect(store.initialState.genericReducer).toEqual({ a: 1 });
11 | expect(store.initialState.todoReducer).toEqual([]);
12 | });
13 |
14 | it("should throw if the reducer passed as argument is not a function or plain object", () => {
15 | expect(() => createStore(false)).toThrow();
16 | expect(() => createStore(undefined)).toThrow();
17 | expect(() => createStore(null)).toThrow();
18 | expect(() => createStore(1)).toThrow();
19 | expect(() => createStore("hello")).toThrow();
20 | expect(() => createStore([])).toThrow();
21 | });
22 |
23 | it("should throw if the opts passed as argument is not a object", () => {
24 | expect(() => createStore(counterReducer, true)).toThrow();
25 | expect(() => createStore(counterReducer, null)).toThrow();
26 | expect(() => createStore(counterReducer, 1)).toThrow();
27 | expect(() => createStore(counterReducer, "hello")).toThrow();
28 | expect(() => createStore(counterReducer, [])).toThrow();
29 | });
30 |
31 | it("should create a store with a preloadedState state", () => {
32 | let opts = { preloadedState: 2, initialAction: { type: "INCREMENT" } };
33 | expect(createStore(counterReducer, opts).initialState).toEqual(3);
34 |
35 | opts = { preloadedState: { counter: 1 } };
36 | expect(createStore(counterReducer, opts).initialState).toEqual(
37 | opts.preloadedState
38 | );
39 |
40 | opts = { preloadedState: "hello" };
41 | expect(createStore(counterReducer, opts).initialState).toEqual(
42 | opts.preloadedState
43 | );
44 |
45 | opts = { preloadedState: null };
46 | expect(createStore(counterReducer, opts).initialState).toEqual(null);
47 |
48 | opts = { preloadedState: false };
49 | expect(createStore(counterReducer, opts).initialState).toEqual(
50 | opts.preloadedState
51 | );
52 |
53 | opts = { preloadedState: [1, 2, 3] };
54 | expect(createStore(counterReducer, opts).initialState).toEqual(
55 | opts.preloadedState
56 | );
57 |
58 | opts = { preloadedState: undefined };
59 | expect(createStore(counterReducer, opts).initialState).toEqual(0);
60 | });
61 |
62 | it("should throw if preloadedState is a function", () => {
63 | const opts = { preloadedState: () => null };
64 | expect(() => createStore(counterReducer, opts)).toThrow();
65 | });
66 |
67 | it("should throw if initialAction is an invalid type", () => {
68 | let opts = { initialAction: null };
69 | expect(() => createStore(counterReducer, opts)).toThrow();
70 |
71 | opts = { initialAction: [] };
72 | expect(() => createStore(counterReducer, opts)).toThrow();
73 |
74 | opts = { initialAction: false };
75 | expect(() => createStore(counterReducer, opts)).toThrow();
76 | });
77 |
78 | it("should create a store with a middlewares", () => {
79 | const opts = { middlewares: [() => null] };
80 | const store = createStore(counterReducer, opts);
81 | expect(store.initialState).toBe(0);
82 | });
83 |
84 | it("should throw if middlewares is an invalid type or if not contain functions as elements", () => {
85 | let opts = { middlewares: null };
86 | expect(() => createStore(counterReducer, opts)).toThrow();
87 |
88 | opts = { middlewares: () => null };
89 | expect(() => createStore(counterReducer, opts)).toThrow();
90 |
91 | opts = { middlewares: 3 };
92 | expect(() => createStore(counterReducer, opts)).toThrow();
93 |
94 | opts = { middlewares: "test" };
95 | expect(() => createStore(counterReducer, opts)).toThrow();
96 |
97 | opts = { middlewares: false };
98 | expect(() => createStore(counterReducer, opts)).toThrow();
99 |
100 | opts = { middlewares: [() => null, undefined, false, 3, "hello"] };
101 | expect(() => createStore(counterReducer, opts)).toThrow();
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/src/connect.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import isPlainObject from "./utils/isPlainObject";
3 | import shallowEqual from "./utils/shallowEqual";
4 | import bindActionCreators from "./bindActionCreators";
5 | import { Context } from "./store";
6 |
7 | /**
8 | * `connect` is a HOC which connects a React Component to the Redhooks store.
9 | * It returns a connected component that wraps the component you passed
10 | * in taking care to avoid unnecessary re-rendering. It should be used if your
11 | * class or function components perform an expensive rendering operation.
12 | *
13 | * @param {Function} mapStateToProps if passed, your component will be subscribed to Redhooks store.
14 | * @param {Function|Object} mapDispatchToProps - if passed must return a plain object whose values are
15 | * functions and whose props will be merged in your component’s props
16 | *
17 | * @returns {Component} It returns a connected component.
18 | */
19 |
20 | const _mock = {};
21 | const _mapStateToProps = () => _mock;
22 | const _mapDispatchToProps = dispatch => ({ dispatch });
23 |
24 | export default (
25 | mapStateToProps = _mapStateToProps,
26 | mapDispatchToProps = _mapDispatchToProps
27 | ) => {
28 | if (mapStateToProps === null) {
29 | mapStateToProps = _mapStateToProps;
30 | }
31 |
32 | if (typeof mapStateToProps !== "function") {
33 | throw new Error(
34 | errMsg("mapStateToProps", mapStateToProps, "must be a function")
35 | );
36 | }
37 |
38 | if (isPlainObject(mapDispatchToProps)) {
39 | mapDispatchToProps = bindFunctionActions(mapDispatchToProps);
40 | }
41 |
42 | if (typeof mapDispatchToProps !== "function") {
43 | throw new Error(
44 | errMsg("mapDispatchToProps", mapDispatchToProps, "must be a function")
45 | );
46 | }
47 |
48 | return Comp =>
49 | class Connect extends PureComponent {
50 | _state = {
51 | changeMapProp: false,
52 | changeOwnProp: false,
53 | prevMapProps: {},
54 | prevOwnProps: this.props
55 | };
56 |
57 | _render = value => {
58 | const { state, dispatch } = value;
59 | const propsMapped = mapStateToProps(state, this.props);
60 |
61 | if (!isPlainObject(propsMapped)) {
62 | throw new Error(
63 | errMsg(
64 | "mapStateToProps",
65 | propsMapped,
66 | "must return a plain object",
67 | Comp.name
68 | )
69 | );
70 | }
71 |
72 | if (!this._dispatchProps) {
73 | const propsDispatch = mapDispatchToProps(dispatch, this.props);
74 | if (!isPlainObject(propsDispatch)) {
75 | throw new Error(
76 | errMsg(
77 | "mapDispatchToProps",
78 | propsDispatch,
79 | "must return a plain object",
80 | Comp.name
81 | )
82 | );
83 | }
84 | this._dispatchProps =
85 | Object.keys(propsDispatch).length > 0
86 | ? propsDispatch
87 | : { dispatch };
88 | }
89 |
90 | const changeMapProp = checkChanges(
91 | propsMapped,
92 | this._state.prevMapProps,
93 | this._state.changeMapProp
94 | );
95 |
96 | const changeOwnProp = checkChanges(
97 | this.props,
98 | this._state.prevOwnProps,
99 | this._state.changeOwnProp
100 | );
101 |
102 | this._state.prevMapProps = propsMapped;
103 | this._state.prevOwnProps = this.props;
104 |
105 | /* istanbul ignore else */
106 | if (
107 | changeMapProp !== this._state.changeMapProp ||
108 | changeOwnProp !== this._state.changeOwnProp ||
109 | !this._wrapper
110 | ) {
111 | this._state.changeMapProp = changeMapProp;
112 | this._state.changeOwnProp = changeOwnProp;
113 | const { _ref, ...props } = this.props;
114 | this._wrapper = (
115 |
121 | );
122 | }
123 |
124 | return this._wrapper;
125 | };
126 |
127 | render() {
128 | return {this._render};
129 | }
130 | };
131 | };
132 |
133 | function checkChanges(newProps, prevProps, status) {
134 | return shallowEqual(newProps, prevProps) ? status : !status;
135 | }
136 |
137 | function bindFunctionActions(mapDispatchToProps) {
138 | return dispatch => bindActionCreators({ ...mapDispatchToProps }, dispatch);
139 | }
140 |
141 | function errMsg(nameFN, prop, addText, displyName = "") {
142 | return `${nameFN}() in Connect(${displyName}) ${addText}. Instead received ${typeof prop} => ${prop}.`;
143 | }
144 |
--------------------------------------------------------------------------------
/__tests__/connect.spec.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, fireEvent } from "react-testing-library";
3 |
4 | import Provider, { connect } from "./../src";
5 | import {
6 | BasicComponent,
7 | BasicComponentNoMapDispatch,
8 | WrapperChangeOwnProp,
9 | IncrementClassCPM
10 | } from "./helpers/components";
11 | import { store } from "./helpers/store";
12 |
13 | const connectComp = (mapStateToProps, mapDispatchToProps) =>
14 | connect(
15 | mapStateToProps,
16 | mapDispatchToProps
17 | )(BasicComponent);
18 |
19 | const connectCompNoMapDispatch = (mapStateToProps, mapDispatchToProps) =>
20 | connect(
21 | mapStateToProps,
22 | mapDispatchToProps
23 | )(BasicComponentNoMapDispatch);
24 |
25 | describe("Utils => connect", () => {
26 | it("should return the function to connect a React Component", () => {
27 | const connector = connect()(BasicComponent);
28 | expect(typeof connector).toBe("function");
29 | });
30 |
31 | it("should connect a React component to the Redhooks store", () => {
32 | const mapStateToProps = state => ({
33 | counterReducer: state.counterReducer
34 | });
35 |
36 | let mapDispatchToProps = dispatch => ({
37 | increment: () => dispatch({ type: "INCREMENT" })
38 | });
39 |
40 | let ConnectedCMP = connectComp(mapStateToProps, mapDispatchToProps);
41 | let wrapper = render(
42 |
43 |
44 |
45 | );
46 | let button = wrapper.container.firstChild;
47 | expect(button.textContent).toBe("0");
48 | fireEvent.click(button);
49 | expect(button.textContent).toBe("1");
50 |
51 | mapDispatchToProps = {
52 | increment: type => ({
53 | type
54 | })
55 | };
56 |
57 | ConnectedCMP = connectComp(mapStateToProps, mapDispatchToProps);
58 | wrapper = render(
59 |
60 |
61 |
62 | );
63 | button = wrapper.container.firstChild;
64 | expect(button.textContent).toBe("0");
65 | fireEvent.click(button);
66 | expect(button.textContent).toBe("1");
67 |
68 | mapDispatchToProps = {
69 | dispatch: action => action,
70 | nested: { add: type => dispatch({ type }) },
71 | ingored: "hi"
72 | };
73 |
74 | ConnectedCMP = connectCompNoMapDispatch(
75 | mapStateToProps,
76 | mapDispatchToProps
77 | );
78 | wrapper = render(
79 |
80 |
81 |
82 | );
83 | button = wrapper.container.firstChild;
84 | expect(button.textContent).toBe("0");
85 | fireEvent.click(button);
86 | expect(button.textContent).toBe("1");
87 |
88 | mapDispatchToProps = dispatch => ({});
89 |
90 | ConnectedCMP = connectCompNoMapDispatch(
91 | mapStateToProps,
92 | mapDispatchToProps
93 | );
94 | wrapper = render(
95 |
96 |
97 |
98 | );
99 | button = wrapper.container.firstChild;
100 | expect(button.textContent).toBe("0");
101 | fireEvent.click(button);
102 | expect(button.textContent).toBe("1");
103 | });
104 |
105 | it("should pass a ref to a connected Component", () => {
106 | const ref = React.createRef();
107 | const name = "test";
108 | render(
109 |
110 |
111 |
112 | );
113 | expect(ref.current).not.toBeNull();
114 | expect(ref.current.props.name).toBe(name);
115 | });
116 |
117 | it("should change the own props of a connected Component", () => {
118 | const ref = React.createRef();
119 | const wrapper = render(
120 |
121 |
122 |
123 | );
124 | ref.current.updateCounter(1);
125 |
126 | let button = wrapper.container.firstChild;
127 | expect(button.textContent).toBe("1");
128 | ref.current.updateCounter(2);
129 | button = wrapper.container.firstChild;
130 | expect(button.textContent).toBe("2");
131 | });
132 |
133 | it("should throw if mapStateToProps returns a wrong type", () => {
134 | spyOn(console, "error"); // In tests that you expect errors
135 | let mapStateToPropsWrong = state => [];
136 | const mapDispatchToProps = dispatch => ({
137 | increment: () => dispatch({ type: "INCREMENT" })
138 | });
139 | let ConnectedCMP = connectComp(mapStateToPropsWrong, mapDispatchToProps);
140 | expect(() =>
141 | render(
142 |
143 |
144 |
145 | )
146 | ).toThrow();
147 |
148 | mapStateToPropsWrong = state => "";
149 | ConnectedCMP = connectComp(mapStateToPropsWrong, mapDispatchToProps);
150 | expect(() =>
151 | render(
152 |
153 |
154 |
155 | )
156 | ).toThrow();
157 |
158 | mapStateToPropsWrong = state => false;
159 | ConnectedCMP = connectComp(mapStateToPropsWrong, mapDispatchToProps);
160 | expect(() =>
161 | render(
162 |
163 |
164 |
165 | )
166 | ).toThrow();
167 |
168 | mapStateToPropsWrong = state => null;
169 | ConnectedCMP = connectComp(mapStateToPropsWrong, mapDispatchToProps);
170 | expect(() =>
171 | render(
172 |
173 |
174 |
175 | )
176 | ).toThrow();
177 |
178 | mapStateToPropsWrong = state => 3;
179 | ConnectedCMP = connectComp(mapStateToPropsWrong, mapDispatchToProps);
180 | expect(() =>
181 | render(
182 |
183 |
184 |
185 | )
186 | ).toThrow();
187 | });
188 |
189 | it("should throw if mapDispatchToProps returns a wrong type", () => {
190 | spyOn(console, "error"); // In tests that you expect errors
191 | const mapStateToProps = state => ({
192 | counterReducer: state.counterReducer
193 | });
194 | let mapDispatchToPropsWrong = dispatch => [];
195 | let ConnectedCMP = connectComp(undefined, mapDispatchToPropsWrong);
196 | expect(() =>
197 | render(
198 |
199 |
200 |
201 | )
202 | ).toThrow();
203 |
204 | mapDispatchToPropsWrong = dispatch => "";
205 | ConnectedCMP = connectComp(null, mapDispatchToPropsWrong);
206 | expect(() =>
207 | render(
208 |
209 |
210 |
211 | )
212 | ).toThrow();
213 |
214 | mapDispatchToPropsWrong = dispatch => false;
215 | ConnectedCMP = connectComp(mapStateToProps, mapDispatchToPropsWrong);
216 | expect(() =>
217 | render(
218 |
219 |
220 |
221 | )
222 | ).toThrow();
223 |
224 | mapDispatchToPropsWrong = dispatch => 3;
225 | ConnectedCMP = connectComp(mapStateToProps, mapDispatchToPropsWrong);
226 | expect(() =>
227 | render(
228 |
229 |
230 |
231 | )
232 | ).toThrow();
233 |
234 | mapDispatchToPropsWrong = dispatch => null;
235 | ConnectedCMP = connectComp(mapStateToProps, mapDispatchToPropsWrong);
236 | expect(() =>
237 | render(
238 |
239 |
240 |
241 | )
242 | ).toThrow();
243 | });
244 |
245 | it("should throw if not valid args are passed", () => {
246 | const noop = () => {};
247 | expect(() => connect({})).toThrow();
248 | expect(() => connect([])).toThrow();
249 | expect(() => connect("")).toThrow();
250 | expect(() => connect(false)).toThrow();
251 | expect(() => connect(3)).toThrow();
252 |
253 | expect(() =>
254 | connect(
255 | noop,
256 | null
257 | )
258 | ).toThrow();
259 | expect(() =>
260 | connect(
261 | noop,
262 | []
263 | )
264 | ).toThrow();
265 | expect(() =>
266 | connect(
267 | noop,
268 | ""
269 | )
270 | ).toThrow();
271 | expect(() =>
272 | connect(
273 | noop,
274 | false
275 | )
276 | ).toThrow();
277 | expect(() =>
278 | connect(
279 | noop,
280 | 3
281 | )
282 | ).toThrow();
283 | });
284 | });
285 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | Redhooks is a tiny React utility library for holding a predictable state container in your React apps.
4 | Inspired by [Redux](https://redux.js.org), it reimplements the redux paradigm of state-management by using React's new Hooks and Context API, which have been [officially released](https://reactjs.org/docs/hooks-reference.html) by the React team.
5 | - [Motivation](#motivation)
6 | - [Basic Example](#basic-example)
7 | - [Apply Middleware](#apply-middleware)
8 | - [Usage with React Router](#usage-with-react-router)
9 | - [Isolating Redhooks Sub-Apps](#isolating-redhooks-sub-apps)
10 | - [Redhooks API Reference](#redhooks-api-reference)
11 | - [CodeSandbox Examples](#codesandbox-examples)
12 | - [License](#license)
13 |
14 | [](https://travis-ci.org/iusehooks/redhooks) [](https://bundlephobia.com/result?p=redhooks) [](https://coveralls.io/github/iusehooks/redhooks?branch=master)  [](https://twitter.com/intent/tweet?text=Predictable%20state%20container%20for%20React%20apps%20written%20using%20Hooks&url=https://github.com/iusehooks/redhooks&hashtags=reactjs,webdev,javascript)
15 |
16 | # Installation
17 | ```sh
18 | npm install --save redhooks
19 | ```
20 |
21 | # Motivation
22 |
23 | In the [Reactjs docs](https://reactjs.org/docs/hooks-custom.html) a nice paragraph titled _useYourImagination()_ suggests to think of different possible usages of functionality Hooks provide, which is essentially what Redhooks tries to do.
24 | This package does not use any third party library, being only dependent upon the Hooks and the Context API.
25 | You do not need to install `react-redux` to connect your components to the store because you can have access to it directly from any of your function components by utilizing the `useStore` Redhooks API.
26 | Hooks are [not allowed within class Components](https://reactjs.org/docs/hooks-rules.html), for using the store within them Redhooks exposes a Higher Order Component (HOC) named `connect`.
27 | It also supports the use of middleware like `redux-thunk` or `redux-saga` or your own custom middleware conforming to the middleware's API.
28 |
29 | # Basic Example
30 |
31 | Redhooks follows the exact same principles of redux, which was the package's inspiration.
32 | * the total state of your app is stored in an object tree inside of a single store object.
33 | * state is _read only_, so the only way to change the state is to dispatch an [action](https://redux.js.org/basics/actions), an object describing the changes to be made to the state.
34 | * to specify how the actions transform the state tree, you write pure reducers.
35 |
36 | ## Store
37 |
38 | `store.js`
39 | ```js
40 | import { createStore, combineReducers } from "redhooks";
41 |
42 | // function reducer
43 | const hello = (
44 | state = { phrase: "good morning" },
45 | { type, payload }
46 | ) => {
47 | switch (type) {
48 | case "SAY_HELLO":
49 | return { ...state, phrase: payload };
50 | default:
51 | return state;
52 | }
53 | };
54 |
55 | // function reducer
56 | const counter = (state = 0, { type, payload }) => {
57 | switch (type) {
58 | case "INCREMENT":
59 | return state + 1;
60 | case "DECREMENT":
61 | return state - 1;
62 | default:
63 | return state;
64 | }
65 | };
66 |
67 | // You can use the combineReducers function
68 | const rootReducer = combineReducers({ hello, counter });
69 | const store = createStore(rootReducer);
70 |
71 | // or if you want to be less verbose you can pass a plain object whose values are reducer functions
72 | const store = createStore({ hello, counter });
73 |
74 | // eventually we can pass to createStore as second arg an opts object like:
75 | // const opts = { preloadedState: { counter: 10 }, initialAction: { type: "INCREMENT" } }
76 | // const store = createStore(rootReducer, opts);
77 |
78 | export default store;
79 | ```
80 |
81 | `App.js`
82 | ```js
83 | import Provider from "redhooks";
84 | import store from "./store";
85 |
86 | function App() {
87 | return (
88 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | const rootElement = document.getElementById("root");
97 | ReactDOM.render(, rootElement);
98 | ```
99 |
100 | ## Dispatching Sync and Async Actions - Non-expensive rendering operation
101 | If your component does not require expensive rendering, you can use the `useStore` Redhooks API within your
102 | functional component in order to access the Redhooks store. Class or function components that perform expensive rendering operations can be connected to the store by using the `connect` Redhooks HOC which takes care to avoid unnecessary re-rendering in order to improve performance. We'll be able to see this in action below:
103 |
104 | `./components/DispatchAction.js`
105 | ```js
106 | import React from "react";
107 | import { useStore } from "redhooks";
108 |
109 | const DispatchAction = () => {
110 | const { dispatch } = useStore();
111 | return (
112 |
113 |
123 |
126 |
133 |
134 | );
135 | };
136 |
137 | export default DispatchAction;
138 | ```
139 |
140 | ## Dispatching Sync and Async Actions - Expensive rendering operation
141 | For components requiring expensive rendering, the use of `connect` helps to avoid any unnecessary re-rendering.
142 |
143 | `./components/DispatchActionExpensive.js`
144 | ```js
145 | import React from "react";
146 | import { connect } from "redhooks";
147 |
148 | const DispatchActionExpensive = props => (
149 |
150 |
153 |
160 |
161 | );
162 |
163 | export default connect()(DispatchActionExpensive);
164 | ```
165 |
166 | ## Use Store from a Function Component
167 |
168 | `./components/ReadFromStore.js`
169 | ```js
170 | import React from "react";
171 | import { useStore } from "redhooks";
172 |
173 | const ReadFromStore = () => {
174 | const { state } = useStore();
175 | const { hello, counter } = state;
176 | return (
177 |
178 | {hello.phrase}
179 | {counter}
180 |
181 | );
182 | };
183 |
184 | export default ReadFromStore;
185 | ```
186 |
187 | > **Tip**: If your function component requires an expensive render, you should use the `connect` HOC Redhooks API.
188 |
189 | ## Use Store from a Class Component
190 |
191 | ```js
192 | import React, { Component } from "react";
193 | import { connect } from "redhooks";
194 |
195 | class ReadFromStore extends Component {
196 | render() {
197 | const { hello, counter } = this.props;
198 | return (
199 |
200 | {hello.phrase}
201 | {counter}
202 |
203 | );
204 | }
205 | };
206 |
207 | function mapStateToProp(state, ownProps) {
208 | return {
209 | hello: state.hello,
210 | counter: state.counter
211 | };
212 | }
213 |
214 | export default connect(mapStateToProp)(ReadFromStore);
215 | ```
216 |
217 | # Apply Middleware
218 |
219 | As for Redux, [middleware](https://redux.js.org/advanced/middleware) is a way to extend Redhooks with custom functionality.
220 | Middleware are functions which receive the store's `dispatch` and `getState` as named arguments, and subsequently return a function. Redhooks supports the use of redux middleware like [redux-thunk](https://www.npmjs.com/package/redux-thunk), [redux-saga](https://www.npmjs.com/package/redux-saga) or you could write custom middleware to conform to the middleware API.
221 |
222 | ## Custom middleware - Logger Example
223 |
224 | ```js
225 | const logger = store => next => action => {
226 | console.log('dispatching', action)
227 | let result = next(action)
228 | console.log('next state', store.getState())
229 | return result
230 | }
231 | ```
232 |
233 | ## Use `redux-thunk` and `redux-saga`
234 |
235 | ```js
236 | import React from "react";
237 | import { render } from "react-dom";
238 | import Provider, { createStore } from "redhooks";
239 | import thunk from "redux-thunk";
240 | import createSagaMiddleware from "redux-saga";
241 | import reducer from "./reducers";
242 |
243 | const sagaMiddleware = createSagaMiddleware();
244 |
245 | const middlewares = [thunk, sagaMiddleware];
246 |
247 | const store = createStore(reducer, { middlewares });
248 |
249 | function* helloSaga() {
250 | console.log("Hello Sagas!");
251 | }
252 | // redux-saga needs to run as soon the store is ready
253 | store.onload = () => sagaMiddleware.run(helloSaga);
254 |
255 | render(
256 |
257 |
258 |
259 | ,
260 | document.getElementById("root")
261 | );
262 | ```
263 |
264 | # Usage with React Router
265 | App routing can be handled using [React Router](https://github.com/ReactTraining/react-router).
266 |
267 | ```js
268 | import React from 'react'
269 | import Provider from 'redhooks'
270 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
271 | import Home from './Home'
272 | import About from './About'
273 |
274 | const App = ({ store }) => (
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 | )
284 |
285 | export default App
286 | ```
287 |
288 | ```js
289 | import React from 'react'
290 | import { render } from 'react-dom'
291 | import { createStore } from 'redhooks'
292 | import thunk from "redux-thunk";
293 | import rootReducer from "./reducers"
294 | import App from './App'
295 |
296 | const opts = {
297 | preloadedState: { counter: 9 },
298 | initialAction: { type: "INCREMENT" },
299 | middlewares: [thunk]
300 | };
301 |
302 | const store = createStore(rootReducer, opts)
303 |
304 | render(, document.getElementById('app'))
305 | ```
306 |
307 | # Isolating Redhooks Sub-Apps
308 |
309 | ```js
310 | import React from "react";
311 | import Provider, { createStore } from "redhooks";
312 | import ReadFromStore from "./components/ReadFromStore";
313 | import Footer from "./components/Footer";
314 |
315 | const counter = (state = 0, { type, payload }) => {
316 | switch (type) {
317 | case "INCREMENT":
318 | return state + 1;
319 | case "DECREMENT":
320 | return state - 1;
321 | default:
322 | return state;
323 | }
324 | };
325 |
326 | const store = createStore(counter);
327 |
328 | export default function SubApp() {
329 | return (
330 |
331 |
332 |
333 |
334 | );
335 | }
336 | ```
337 |
338 | Each instance will be independent and it will have its own store.
339 |
340 | ```js
341 | import React from "react";
342 | import SubApp from "./SubApp";
343 |
344 | function App() {
345 | return (
346 |
347 |
348 |
349 |
350 | );
351 | }
352 |
353 | const rootElement = document.getElementById("root");
354 | ReactDOM.render(, rootElement);
355 | ```
356 |
357 | # Redhooks API Reference
358 |
359 | * [createStore](#createStore)
360 | * [combineReducers](#combineReducers)
361 | * [connect](#connect)
362 | * [bindActionCreators](#bindActionCreators)
363 | * [Provider](#Provider)
364 | * [useStore](#useStore)
365 |
366 | ## createStore
367 | ```js
368 | createStore(reducer, [opts])
369 | ```
370 | `createStore` returns the store object to be passed to the ``.
371 | * The `reducer` argument might be a single reducer function, a function returned by `combineReducers` or a plain object whose values are reducer functions (if your store requires multiple reducers).
372 | * The `opts` optional argument is an object which allows you to pass a `preloadedState`, `initialAction` and `middlewares`.
373 | > The store is ready when the `Provider` is mounted, after which an `onload` event will be triggered.
374 |
375 | #### Example
376 | ```js
377 | const opts = {
378 | preloadedState: 1,
379 | initialAction: { type: "DECREMENT" },
380 | middlewares: [thunk, sagaMiddleware, logger]
381 | };
382 | const store = createStore(reducer, opts);
383 | store.onload = ({ dispatch }) => dispatch({ type: "INCREMENT" });
384 | ```
385 |
386 | ## combineReducers
387 | ```js
388 | combineReducers(reducers)
389 | ```
390 | `combineReducers` combines an object whose props are different reducer functions, into a single reducer function
391 | * The `reducers` argument is an object whose values correspond to different reducing functions that need to be combined into one.
392 |
393 | #### Example
394 | ```js
395 | const rootReducer = combineReducers({ counter, otherReducer })
396 | const store = createStore(rootReducer)
397 | ```
398 |
399 | ## connect
400 | ```js
401 | connect([mapStateToProps], [mapDispatchToProps])
402 | ```
403 | `connect` function connects a React component to a Redhooks store. It returns a connected component class that wraps the component you passed in taking care to avoid unnecessary re-rendering. It should be used if your class or function components require expensive rendering.
404 |
405 | * If a `mapStateToProps` function is passed, your component will be subscribed to Redhooks store. Any time the store is updated, `mapStateToProps` will be called. The results of `mapStateToProps` must be a plain object, which will be merged into your component’s props. If you don't want to connect to Redhooks store, pass null or undefined in place of mapStateToProps.
406 | * `mapDispatchToProps`, if passed, may be either a function that returns a plain object whose values, themselves, are functions or a plain object whose values are [action creator](#action-creator) functions. In both cases the props of the returned object will be merged in your component’s props. If is not passed your component will receive `dispatch` prop by default.
407 |
408 | #### Example
409 | ```js
410 | const mapStateToProps = (state, ownProps) => ({ counter: state.counter })
411 | const mapDispatchToProps = dispatch => ({ increment: action => dispatch({ type: action })})
412 | // or
413 | const mapDispatchToProps = { increment: type => ({ type })}
414 |
415 | export default connect(mapStateToProps, mapDispatchToProps)(ReactComponent)
416 | ```
417 |
418 | ## bindActionCreators
419 | ```js
420 | bindActionCreators(actionCreators, dispatch)
421 | ```
422 |
423 | `bindActionCreators` turns an object whose values are [action creators](#action-creator) into an object with the
424 | same keys, but with every function wrapped in a `dispatch` call so they
425 | may be invoked directly.
426 |
427 | * `actionCreators` An object whose values are action creator functions or plain objects whose values are action creator functions
428 | * `dispatch` The dispatch function available on your Redhooks store.
429 |
430 | #### Action creator
431 | An action creator is a function that creates an action.
432 |
433 | ```js
434 | type ActionCreator = (...args: any) => Action | AsyncAction
435 | ```
436 |
437 | #### Example
438 | `actions.js`
439 |
440 | ```js
441 | export const action_1 = action_1 => action_1
442 | export const action_2 = action_2 => action_2
443 | export const action_3 = action_3 => action_3
444 | ```
445 |
446 | `YourComponentConnected.js`
447 | ```js
448 | import React from "react";
449 | import { connect, bindActionCreators } from "redhooks";
450 | import * as actions from "./actions";
451 |
452 | const YourComponent = ({ actions, counter }) => (
453 |
454 |
counter
455 |
456 |
457 |
458 |
459 | );
460 |
461 | const mapStateToProps = state => ({
462 | counter: state.counter
463 | });
464 |
465 | // a verbose way
466 | const mapDispatchToProps = dispatch => ({
467 | actions: bindActionCreators(actions, dispatch)
468 | });
469 | export default connect(
470 | mapStateToProps,
471 | mapDispatchToProps
472 | )(YourComponent);
473 |
474 | // or simply
475 | export default connect(
476 | mapStateToProps,
477 | { actions }
478 | )(YourComponent);
479 | ```
480 |
481 | ## Provider
482 | The `` makes the Redhooks store available to any nested components.
483 |
484 | #### Example
485 | ```js
486 | import React from "react";
487 | import Provider from "redhooks";
488 | import store from "./store";
489 | import ReadFromStore from "./components/ReadFromStore";
490 | import Footer from "./components/Footer";
491 |
492 | export default function App() {
493 | return (
494 |
495 |
496 |
497 |
498 | );
499 | }
500 | ```
501 |
502 | ## useStore
503 | ```js
504 | useStore()
505 | ```
506 | `useStore` can be only used within a function component and it returns the `store`.
507 | * The `store` is an object whose props are the `state` and the `dispatch`.
508 |
509 | #### Example
510 | ```js
511 | import React from "react";
512 | import { useStore } from "redhooks";
513 |
514 | const Example = () => {
515 | const { state, dispatch } = useStore(); // do not use it within a Class Component
516 | const { counter } = state;
517 | return (
518 |
519 | {counter}
520 |
523 |
524 | );
525 | };
526 |
527 | export default Example;
528 | ```
529 |
530 | # CodeSandbox Examples
531 |
532 | Following few open source projects implemented with `redux` have been migrated to `redhooks`:
533 |
534 | * Shopping Cart: [Sandbox](https://codesandbox.io/s/5yn1258y4l)
535 | * TodoMVC: [Sandbox](https://codesandbox.io/s/7jyq991p90)
536 | * Tree-View: [Sandbox](https://codesandbox.io/s/rmw98onnlp)
537 | * Saga-Middleware: [Sandbox](https://codesandbox.io/s/48pomo7rx7)
538 | * Redux-Thunk: [Sandbox](https://codesandbox.io/s/n02r5400mp)
539 |
540 | # License
541 |
542 | This software is free to use under the MIT license.
543 | See the [LICENSE file](/LICENSE.md) for license text and copyright information.
544 |
--------------------------------------------------------------------------------