Skip to content

Added support for createStore to accept a reducer function #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/migration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.parcel-cache/
dist/
3 changes: 3 additions & 0 deletions examples/migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Migration example

Example showing how to add H4R to an existing app
1,667 changes: 1,667 additions & 0 deletions examples/migration/package-lock.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions examples/migration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "1.0.0",
"private": true,
"scripts": {
"start": "parcel src/index.html"
},
"devDependencies": {
"parcel": "^2.4.0"
},
"dependencies": {
"hooks-for-redux": "^2.0.4"
}
}
52 changes: 52 additions & 0 deletions examples/migration/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {setStore, createStore, createReduxModule} from 'hooks-for-redux'

// Current reducer used in your app
const reducers = (state, action) => {
switch (action.type) {
case 'INCREMENT_COUNT':
return {...state, count: (state.count || 42) + 1}
case 'DECREMENT_COUNT':
return {...state, count: (state.count || 42) - 1}
default:
return state
}
}

// Set store with H4R
const store = setStore(createStore(reducers))

// Add existing state slice with H4R (slice will be changeable with dispatch and H4R hooks)
const [useCount, {increment, decrement}] = createReduxModule('count', 42, {
increment: (state) => state + 1,
decrement: (state) => state - 1,
})

// Add non-existing state slice with H4R (slice will be changeable with H4R hooks)
const [useName, {setName}] = createReduxModule('name', 'Marty', {
setName: (state, name) => name || 'Marty',
})

let render = () => {
console.log(store.getState())
document.getElementById('count-state').innerHTML = store.getState().count
document.getElementById('name-state').innerHTML = store.getState().name
}

store.subscribe(render)

// Change count using dispatch
document
.getElementById('increment-dispatch')
.addEventListener('click', () => store.dispatch({type: 'INCREMENT_COUNT'}))

// Change count using hook
document
.getElementById('decrement-hook')
.addEventListener('click', decrement)

// Change name using hook
document
.getElementById('name-input')
.addEventListener('keyup', (e) => setName(e.target.value))

render()
20 changes: 20 additions & 0 deletions examples/migration/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>H4R Migration Example</title>
<link rel="stylesheet" href="styles.css" />
<script type="module" src="app.js"></script>
</head>
<body>
<div id="root">
<h2>Hello <span id="name-state"></span>, you are <span id="count-state"></span>!</h2>
<hr />
<input id="name-input" type="text" placeholder="Name" />
<hr />
<button id="decrement-hook">Decrement with hook</button>
<button id="increment-dispatch">Increment with dispatch</button>
</div>
</body>
</html>
73 changes: 73 additions & 0 deletions examples/migration/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
html {
height: 100%;
padding: 0px;
}

body {
display: flex;
height: 100%;
font-family: Verdana, sans-serif;
align-items: center;
justify-content: center;
background-color: #FAFAFA;
}

button {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
color: inherit;
font: inherit;
line-height: normal;
user-select: none;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;

padding: 15px;
border-radius: 3px;
font-size: 12px;
text-transform: uppercase;
cursor: pointer;
transition: .3s;
background-color: #1B754D;
color: #FFFFFF;
}

button:hover {
background: #1E8557;
}

button::-moz-focus-inner {
border: 0;
padding: 0;
}

hr {
margin: 20px 0;
border-top: 1px solid #D8D8D8;
border-bottom: none;
}

#root {
padding: 50px;
border: 1px solid rgba(27,117,77,0.2);
border-radius: 5px;
background-color: #FFFFFF;
}

input {
width: 100%;
padding: 15px;
border: 1px solid rgba(27,117,77,0.2);
box-sizing: border-box;
outline: none;
border-radius: 3px;
}

input:focus {
border: 1px solid rgba(27,117,77,0.8);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@
"type": "git",
"url": "https://github.com/generalui/hooks-for-redux.git"
},
"version": "2.0.3"
"version": "2.0.4"
}
81 changes: 70 additions & 11 deletions src/createStore.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,77 @@
const { createStore: reduxCreateStore, combineReducers } = require('redux')

module.exports = (initialReducers = {}, ...args) => {
if (typeof initialReducers !== "object") {
console.error({initialReducers, args})
throw new Error("initialReducers should be an object suitable to be passed to combineReducers")
}
const getStateSlices = (state, reducerKeys, initialKeys = []) => {
const slices = { known: {}, unknown: {} }

Object.keys(state).forEach((key) => {
if (reducerKeys.includes(key)) {
slices.known[key] = state[key]
} else {
slices.unknown[key] = state[key]
}

if (initialKeys.includes(key)) {
slices.unknown[key] = state[key]
}
})

return slices
}

module.exports = (initialReducer = {}, ...args) => {
let store
let reducers

// Passing an object of reducers is preferable for performances
if (typeof initialReducer === "object") {
reducers = {...initialReducer, _stub_: (s) => s || 0}
store = reduxCreateStore(combineReducers(reducers))
store.injectReducer = (key, reducer) => {
if (reducers[key]) console.warn(`injectReducer: replacing reducer for key '${key}'`)
reducers[key] = reducer
store.replaceReducer(combineReducers(reducers))
}
}
// Support for default redux API (single or combined reducer)
else if (typeof initialReducer === "function") {
const initialKeys = Object.keys(initialReducer(undefined, '') || {})
reducers = {_stub_: (s) => s || 0}
let reducerKeys = Object.keys(reducers)
let combinedReducer = combineReducers(reducers)
let rootReducer

const reducers = {...initialReducers, _stub_: (s) => s || 0}
const store = reduxCreateStore(combineReducers(reducers), ...args)
// Combined reducer
if (initialReducer.name === 'combination') {
rootReducer = (state = {}, action) => {
const slices = getStateSlices(state, reducerKeys, initialKeys)
const intermediate = combinedReducer(slices.known, action)
Object.keys(intermediate).forEach((key) => {
if (slices.unknown.hasOwnProperty(key)) {
slices.unknown[key] = intermediate[key]
}
})
return {intermediate, ...initialReducer(slices.unknown, action)}
}
}
// Single reducer
else {
rootReducer = (state = {}, action) => {
const slices = getStateSlices(state, reducerKeys, initialKeys)
return initialReducer({...state, ...combinedReducer(slices.known, action)}, action)
}
}

store.injectReducer = (key, reducer) => {
if (reducers[key]) console.warn(`injectReducer: replacing reducer for key '${key}'`);
reducers[key] = reducer
store.replaceReducer(combineReducers(reducers))
store = reduxCreateStore(rootReducer)
store.injectReducer = (key, reducer) => {
if (reducers[key]) console.warn(`injectReducer: replacing reducer for key '${key}'`)
reducers[key] = reducer
reducerKeys = Object.keys(reducers)
combinedReducer = combineReducers(reducers)
store.replaceReducer(rootReducer)
}
} else {
console.error({initialReducer, args})
throw new Error("initialReducer should be an object suitable to be passed to combineReducers or a reducing function (\"uncombined\")")
}

return store
Expand Down
98 changes: 97 additions & 1 deletion src/tests/createStore.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,100 @@
import { getStore } from "../index";
import renderer from 'react-test-renderer';
const { combineReducers } = require('redux')
import { Provider, setStore, getStore, createStore, createReduxModule } from "../index";

describe("support default createStore API", () => {

const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT_COUNT':
return {...state, count: (state.count || 0) + 1}
default:
return state
}
}

const store = setStore(createStore(reducer))

const [useCount, {increment, decrement}] = createReduxModule('count', 5, {
increment: (state) => state + 1,
decrement: (state) => state - 1,
}, store)

const [useMessage, {setMessage}] = createReduxModule('message', 'Hello!', {
setMessage: (state, message) => message,
}, store)

it("initialize state", () => {
expect(store.getState().count).toEqual(5)
expect(store.getState().message).toEqual('Hello!')
})

it("increment count with dispatch", () => {
renderer.act(() => {store.dispatch({type: 'INCREMENT_COUNT'})})
expect(store.getState().count).toEqual(6);
})

it("increment count with hook", () => {
renderer.act(() => {increment()})
expect(store.getState().count).toEqual(7);
})

it("set message with hook", () => {
renderer.act(() => {setMessage('Goodbye!')})
expect(store.getState().message).toEqual('Goodbye!');
})
});


describe("support default createStore API with combined reducers", () => {

const countReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT_COUNT':
return state + 1
default:
return state
}
}

const messageReducer = (state = 'Hello!', action) => {
switch (action.type) {
case 'SET_MESSAGE':
return state = action.payload
default:
return state
}
}

const combinedReducer = combineReducers({count: countReducer, message: messageReducer})
const store = createStore(combinedReducer)

const [useCount, {increment, decrement}] = createReduxModule('count', 5, {
increment: (state) => state + 1,
decrement: (state) => state - 1,
}, store)

it("initialize state", () => {
// Initial state set with createReduxModule is not applied if reducer already exists
expect(store.getState().count).toEqual(0)
expect(store.getState().message).toEqual('Hello!')
})

it("increment count with dispatch", () => {
renderer.act(() => {store.dispatch({type: 'INCREMENT_COUNT'})})
expect(store.getState().count).toEqual(1);
})

it("set message with dispatch", () => {
renderer.act(() => {store.dispatch({type: 'SET_MESSAGE', payload: 'Goodbye!'})})
expect(store.getState().message).toEqual('Goodbye!');
})

it("increment count with hook", () => {
renderer.act(() => {increment()})
expect(store.getState().count).toEqual(2);
})
});

it("injectReducer key must be unique", () => {
getStore().injectReducer("myKey", () => 1);
Expand Down