/node_modules/"
133 | ],
134 | "snapshotSerializers": [
135 | "enzyme-to-json/serializer"
136 | ]
137 | },
138 | "config": {
139 | "commitizen": {
140 | "path": "./node_modules/cz-conventional-changelog"
141 | }
142 | },
143 | "moduleRoots": [
144 | "src"
145 | ]
146 | }
147 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
3 | import shallowEqual from './shallowEqual';
4 |
5 | export const ReduxContext = createContext({});
6 |
7 | export const Provider = ({ store, children }) => (
8 | {children}
9 | );
10 |
11 | export const useReduxCore = (selector, { shouldHooksUpdate }) => {
12 | const store = useContext(ReduxContext);
13 | const runGetState = () => selector(store.getState());
14 |
15 | const [state, setState] = useState(runGetState);
16 |
17 | const lastStore = useRef(store);
18 | const lastSelector = useRef(selector);
19 | const lastUpdateState = useRef(state);
20 |
21 | function handleChange() {
22 | const updateState = runGetState();
23 |
24 | // Can custom setup shallowEqual method on shouldHooksUpdate
25 | const shouldUpdate =
26 | typeof shouldHooksUpdate === 'function'
27 | ? shouldHooksUpdate(updateState, lastUpdateState.current)
28 | : !shallowEqual(updateState, lastUpdateState.current);
29 |
30 | if (shouldUpdate) {
31 | setState(updateState);
32 | lastUpdateState.current = updateState;
33 | }
34 | }
35 |
36 | if (lastStore.current !== store || lastSelector.current !== selector) {
37 | lastStore.current = store;
38 | lastSelector.current = selector;
39 |
40 | handleChange();
41 | }
42 |
43 | useEffect(() => {
44 | let didUnsubscribe = false;
45 |
46 | const checkForUpdates = () => {
47 | if (didUnsubscribe) {
48 | return;
49 | }
50 |
51 | handleChange();
52 | };
53 |
54 | checkForUpdates();
55 |
56 | const unsubscribe = store.subscribe(checkForUpdates);
57 | return () => {
58 | didUnsubscribe = true;
59 | unsubscribe();
60 | };
61 | }, [store, selector]);
62 |
63 | return [state, store.dispatch];
64 | };
65 |
66 | const defaultSelector = state => state;
67 |
68 | export const useRedux = (originSelector, actions, options = {}) => {
69 | const selector = typeof originSelector !== 'function' ? defaultSelector : originSelector;
70 |
71 | const [state, dispatch] = useReduxCore(selector, options);
72 |
73 | if (typeof actions === 'undefined' || actions === null) {
74 | return [state, dispatch];
75 | }
76 |
77 | const boundActions =
78 | typeof actions === 'function' ? actions(dispatch) : bindActionCreators(actions, dispatch);
79 |
80 | return [state, boundActions];
81 | };
82 |
--------------------------------------------------------------------------------
/src/shallowEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is Copy from https://github.com/facebook/react/blob/master/packages/shared/shallowEqual.js
3 | *
4 | * Copyright (c) Facebook, Inc. and its affiliates.
5 | *
6 | * This source code is licensed under the MIT license found in the
7 | * LICENSE file in the root directory of this source tree.
8 | */
9 |
10 | /**
11 | * inlined Object.is polyfill to avoid requiring consumers ship their own
12 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
13 | */
14 | function is(x, y) {
15 | if (x === y) {
16 | return x !== 0 || 1 / x === 1 / y;
17 | }
18 |
19 | return x !== x && y !== y; // eslint-disable-line no-self-compare
20 | }
21 |
22 | const { hasOwnProperty } = Object.prototype;
23 |
24 | /**
25 | * Performs equality by iterating through keys on an object and returning false
26 | * when any key has values which are not strictly equal between the arguments.
27 | * Returns true when the values of all keys are strictly equal.
28 | */
29 | function shallowEqual(objA, objB) {
30 | if (is(objA, objB)) {
31 | return true;
32 | }
33 |
34 | if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
35 | return false;
36 | }
37 |
38 | const keysA = Object.keys(objA);
39 | const keysB = Object.keys(objB);
40 |
41 | if (keysA.length !== keysB.length) {
42 | return false;
43 | }
44 |
45 | // Test for A's keys different from B.
46 | for (let i = 0; i < keysA.length; i += 1) {
47 | if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
48 | return false;
49 | }
50 | }
51 |
52 | return true;
53 | }
54 |
55 | export default shallowEqual;
56 |
--------------------------------------------------------------------------------
/storybook/__conf__/mockConfig.js:
--------------------------------------------------------------------------------
1 | jest.mock('../facade');
2 |
--------------------------------------------------------------------------------
/storybook/__conf__/polyfill.js:
--------------------------------------------------------------------------------
1 | global.requestAnimationFrame = callback => {
2 | setTimeout(callback, 0);
3 | };
4 |
--------------------------------------------------------------------------------
/storybook/__conf__/setup.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/storybook/__mocks__/facade.js:
--------------------------------------------------------------------------------
1 | import { mount } from 'enzyme';
2 | import expect from 'expect';
3 |
4 | const createSnapshot = (name, story) => {
5 | it(name, () => {
6 | expect(mount(story)).toMatchSnapshot();
7 | });
8 | };
9 |
10 | export const storiesOf = function storiesOf() {
11 | const api = {};
12 | let story;
13 |
14 | api.add = (name, func, { ignoreTest } = { ignoreTest: false }) => {
15 | if (!ignoreTest) {
16 | story = func();
17 | createSnapshot(name, story);
18 | } else {
19 | it(name, () => {});
20 | }
21 | return api;
22 | };
23 |
24 | api.addWithInfo = (name, func) => {
25 | story = func();
26 | createSnapshot(name, story);
27 | return api;
28 | };
29 |
30 | api.addDecorator = () => {};
31 |
32 | return api;
33 | };
34 |
--------------------------------------------------------------------------------
/storybook/__mocks__/file.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-knobs/register';
3 |
--------------------------------------------------------------------------------
/storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | const req = require.context('./examples/', true, /stories\.js$/);
4 |
5 | function loadStories() {
6 | req.keys().forEach(req);
7 | }
8 | configure(loadStories, module);
9 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/__tests__/TodoList.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withKnobs } from '@storybook/addon-knobs/react';
3 | import { storiesOf } from '@storybook/react';
4 |
5 | import TodoList from '../index.js';
6 |
7 | const stories = storiesOf('TodoList', module);
8 |
9 | stories.addDecorator(withKnobs);
10 |
11 | stories.add('__interactive', () => );
12 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/actions/index.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0;
2 |
3 | export const addTodo = text => {
4 | nextTodoId += 1;
5 |
6 | return {
7 | type: 'ADD_TODO',
8 | id: nextTodoId,
9 | text,
10 | };
11 | };
12 |
13 | export const setVisibilityFilter = filter => ({
14 | type: 'SET_VISIBILITY_FILTER',
15 | filter,
16 | });
17 |
18 | export const toggleTodo = id => ({
19 | type: 'TOGGLE_TODO',
20 | id,
21 | });
22 |
23 | export const VisibilityFilters = {
24 | SHOW_ALL: 'SHOW_ALL',
25 | SHOW_COMPLETED: 'SHOW_COMPLETED',
26 | SHOW_ACTIVE: 'SHOW_ACTIVE',
27 | };
28 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FilterLink from '../containers/FilterLink';
3 | import { VisibilityFilters } from '../actions';
4 |
5 | const Footer = () => (
6 |
7 | Show:
8 | All
9 | Active
10 | Completed
11 |
12 | );
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Todo = ({ onClick, completed, text }) => (
5 |
6 |
14 |
15 | );
16 |
17 | Todo.propTypes = {
18 | onClick: PropTypes.func.isRequired,
19 | completed: PropTypes.bool.isRequired,
20 | text: PropTypes.string.isRequired,
21 | };
22 |
23 | export default Todo;
24 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/containers/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as actionCreators from '../actions';
3 |
4 | import { useRedux } from '../../../../src';
5 |
6 | const useAddTodoAction = () => useRedux(undefined, actionCreators);
7 |
8 | const AddTodo = () => {
9 | const [, { addTodo }] = useAddTodoAction();
10 |
11 | let input;
12 |
13 | return (
14 |
15 |
32 |
33 | );
34 | };
35 |
36 | export default AddTodo;
37 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/containers/FilterLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import * as actionCreators from '../actions';
5 | import { useRedux } from '../../../../src';
6 |
7 | const useFilterLink = filter =>
8 | useRedux(({ visibilityFilter }) => visibilityFilter === filter, actionCreators);
9 |
10 | const Link = ({ filter, children }) => {
11 | const [active, { setVisibilityFilter }] = useFilterLink(filter);
12 |
13 | return (
14 |
23 | );
24 | };
25 |
26 | Link.propTypes = {
27 | filter: PropTypes.string.isRequired,
28 | children: PropTypes.node.isRequired,
29 | };
30 |
31 | export default Link;
32 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/containers/VisibleTodoList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useRedux } from '../../../../src';
4 |
5 | import { toggleTodo, VisibilityFilters } from '../actions';
6 |
7 | import Todo from '../components/Todo';
8 |
9 | const getVisibleTodos = ({ todos, visibilityFilter }) => {
10 | switch (visibilityFilter) {
11 | case VisibilityFilters.SHOW_ALL:
12 | return todos;
13 | case VisibilityFilters.SHOW_COMPLETED:
14 | return todos.filter(t => t.completed);
15 | case VisibilityFilters.SHOW_ACTIVE:
16 | return todos.filter(t => !t.completed);
17 | default:
18 | throw new Error(`Unknown filter: ${visibilityFilter}`);
19 | }
20 | };
21 |
22 | const useTodoList = () => useRedux(getVisibleTodos, { toggleTodo }, { pure: true });
23 |
24 | const TodoList = () => {
25 | const [todos, actions] = useTodoList();
26 |
27 | return (
28 |
29 | {todos.map(todo => (
30 | actions.toggleTodo(todo.id)} />
31 | ))}
32 |
33 | );
34 | };
35 |
36 | export default TodoList;
37 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/index.js:
--------------------------------------------------------------------------------
1 | // This example is from redux official website
2 | // https://redux.js.org/basics/example
3 | // I override it to use react-redux-hooks
4 |
5 | import React from 'react';
6 | import { createStore } from 'redux';
7 |
8 | import { Provider } from '../../../src';
9 |
10 | import Footer from './components/Footer';
11 | import AddTodo from './containers/AddTodo';
12 | import VisibleTodoList from './containers/VisibleTodoList';
13 |
14 | import rootReducer from './reducers';
15 |
16 | const store = createStore(rootReducer);
17 |
18 | const App = () => (
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import todos from './todos';
3 | import visibilityFilter from './visibilityFilter';
4 |
5 | export default combineReducers({
6 | todos,
7 | visibilityFilter,
8 | });
9 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/reducers/todos.js:
--------------------------------------------------------------------------------
1 | const todos = (state = [], action) => {
2 | switch (action.type) {
3 | case 'ADD_TODO':
4 | return [
5 | ...state,
6 | {
7 | id: action.id,
8 | text: action.text,
9 | completed: false,
10 | },
11 | ];
12 | case 'TOGGLE_TODO':
13 | return state.map(
14 | todo => (todo.id === action.id ? { ...todo, completed: !todo.completed } : todo),
15 | );
16 | default:
17 | return state;
18 | }
19 | };
20 |
21 | export default todos;
22 |
--------------------------------------------------------------------------------
/storybook/examples/TodoList/reducers/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | const visibilityFilter = (state = 'SHOW_ALL', action) => {
2 | switch (action.type) {
3 | case 'SET_VISIBILITY_FILTER':
4 | return action.filter;
5 | default:
6 | return state;
7 | }
8 | };
9 |
10 | export default visibilityFilter;
11 |
--------------------------------------------------------------------------------
/storybook/examples/ToggleButton/__tests__/ToggleButton.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 |
4 | import ToggleButton from '../index.js';
5 |
6 | test('should have default value: Click to open', () => {
7 | const component = mount();
8 | expect(component.text()).toContain('Click to open');
9 | });
10 |
11 | // Create custom snapshot testing
12 | test('should become "Click to close" after click', () => {
13 | const component = mount();
14 |
15 | expect(component).toMatchSnapshot();
16 |
17 | // manually trigger the callback
18 | component.find('button').simulate('click');
19 |
20 | // enzyme not support sideEffect yet
21 | // expect(component.text()).toContain('Click to close');
22 | // // re-rendering would become Open
23 | // expect(component).toMatchSnapshot();
24 |
25 | // // manually trigger the callback
26 | // component.find('button').simulate('click');
27 |
28 | // expect(component.text()).toContain('Click to open');
29 | // // re-rendering would become Close
30 | // expect(component).toMatchSnapshot();
31 | });
32 |
--------------------------------------------------------------------------------
/storybook/examples/ToggleButton/__tests__/ToggleButton.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withKnobs } from '@storybook/addon-knobs/react';
3 | import { storiesOf } from '@storybook/react';
4 |
5 | import ToggleButton from '../index.js';
6 |
7 | const stories = storiesOf('ToggleButton', module);
8 |
9 | stories.addDecorator(withKnobs);
10 |
11 | stories.add('__interactive', () => );
12 |
--------------------------------------------------------------------------------
/storybook/examples/ToggleButton/__tests__/__snapshots__/ToggleButton.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should become "Click to close" after click 1`] = `
4 |
5 |
16 |
17 |
22 |
23 |
24 |
25 | `;
26 |
--------------------------------------------------------------------------------
/storybook/examples/ToggleButton/__tests__/__snapshots__/ToggleButton.stories.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`__interactive 1`] = `
4 |
10 |
16 |
21 |
22 |
23 | `;
24 |
--------------------------------------------------------------------------------
/storybook/examples/ToggleButton/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStore } from 'redux';
3 |
4 | import { Provider, useRedux } from '../../../src';
5 |
6 | const store = createStore((state = { toggle: false }, action) => {
7 | if (action.type === 'TOGGLE') {
8 | return { toggle: !state.toggle };
9 | }
10 |
11 | return state;
12 | });
13 |
14 | const ToggleButton = () => {
15 | const [state, dispatch] = useRedux();
16 |
17 | return (
18 |
21 | );
22 | };
23 |
24 | const RootComponent = () => (
25 |
26 |
27 |
28 | );
29 |
30 | export default RootComponent;
31 |
--------------------------------------------------------------------------------
/storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | require('@babel/register');
2 |
3 | const path = require('path');
4 |
5 | module.exports = {
6 | module: {
7 | rules: [
8 | {
9 | test: /\.js?$/,
10 | exclude: path.join(__dirname, 'node_modules'),
11 | loader: 'babel-loader',
12 | options: {
13 | babelrc: false,
14 | presets: [
15 | ['@babel/preset-env', { loose: true, modules: false, useBuiltIns: 'entry' }],
16 | '@babel/preset-react',
17 | ],
18 | plugins: [
19 | 'react-hot-loader/babel',
20 | '@babel/plugin-syntax-dynamic-import',
21 | '@babel/plugin-syntax-import-meta',
22 | '@babel/plugin-proposal-class-properties',
23 | '@babel/plugin-proposal-json-strings',
24 | '@babel/plugin-transform-react-constant-elements',
25 | ],
26 | },
27 | },
28 | ],
29 | },
30 | externals: {
31 | jsdom: 'window',
32 | cheerio: 'window',
33 | 'react/lib/ExecutionEnvironment': true,
34 | 'react/lib/ReactContext': 'window',
35 | 'react/addons': true,
36 | },
37 | };
38 |
--------------------------------------------------------------------------------