├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── READme.md ├── babel.config.js ├── core └── index.js ├── example └── simple-app │ ├── actions.js │ ├── components │ ├── Picker.js │ └── Posts.js │ ├── containers │ ├── App.js │ └── Root.js │ ├── index.html │ ├── index.js │ ├── reducers.js │ └── store.js ├── index.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── react-redux └── index.js ├── redux-devtools-extension └── index.js ├── redux-logger └── index.js ├── redux-thunk └── index.js └── redux └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cache 3 | 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .idea 3 | 4 | dist 5 | example 6 | 7 | babel.config.js 8 | prettier.config.js 9 | package-lock.json 10 | 11 | CHANGELOG.md 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [1.0.0-alpha4] - 2020-10-02 8 | 9 | ### Added 10 | 11 | - FAQ to READme.md 12 | 13 | ### Fixed 14 | 15 | - parcel alias for examples 16 | - jsx was removed 17 | -------------------------------------------------------------------------------- /READme.md: -------------------------------------------------------------------------------- 1 | # storeonize 2 | 3 | easy way to migrate from Redux to [Storeon](https://github.com/storeon/storeon) 4 | 5 | ## how to 6 | 7 | `npm i -S storeonize` 8 | 9 | just add `storeonize` to your imports 10 | 11 | ```js 12 | // before 13 | import { createStore, applyMiddleware } from 'redux' 14 | 15 | // after 16 | import { createStore, applyMiddleware } from 'storeonize/redux' 17 | ``` 18 | 19 | ## what things can be storeonized? 20 | 21 | * redux 22 | * [x] createStore 23 | * [x] applyMiddleware 24 | * [x] combineReducers 25 | 26 | * redux-logger 27 | * [x] createLogger 28 | 29 | * redux-devtools-extension 30 | * [x] composeWithDevTools 31 | 32 | * react-redux 33 | * [x] Provider 34 | * [x] connect 35 | 36 | ## alert! 37 | 38 | `combineReducers` is not the same as redux's 39 | 40 | the only arguments is a map: 41 | 42 | ``` 43 | combineReducers({ 44 | someReducerName: [reducerFunction, [array of connected actions] 45 | }) 46 | ``` 47 | 48 | ## faq 49 | 50 | ##### Storeonize just swaps Redux with Storeon. How can I start coding in Storeon style? 51 | 52 | First of all, you can use your own modules passing them to `applyMiddlware`. 53 | I suggest you to start rewriting code in this way: reducers, then middlewares. 54 | 55 | 56 | ## todo 57 | 58 | * [ ] typings 59 | * [ ] other redux-stuff 60 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-react'], 3 | } 4 | -------------------------------------------------------------------------------- /core/index.js: -------------------------------------------------------------------------------- 1 | const morphReducer = (reducerName, reducer, actions = []) => { 2 | return store => { 3 | store.on('@init', s => { 4 | const newState = reducer(undefined, {}) 5 | 6 | return { 7 | store: { 8 | ...s.store, 9 | [reducerName]: newState, 10 | }, 11 | } 12 | }) 13 | 14 | actions.forEach(action => { 15 | store.on(action, ({ store }, payload) => { 16 | const newState = reducer(store, { type: action, ...payload }) 17 | 18 | return { 19 | store: { 20 | ...store, 21 | [reducerName]: newState, 22 | }, 23 | } 24 | }) 25 | }) 26 | } 27 | } 28 | 29 | const customDispatcher = (dispatch, store) => { 30 | const d = (...dispatchArgs) => { 31 | const [event] = dispatchArgs 32 | 33 | if (typeof event === 'function') { 34 | event(d, () => store) 35 | } else if (typeof event === 'object' && event.type) { 36 | const { type, ...restEventData } = event 37 | 38 | dispatch(event.type, restEventData) 39 | } else { 40 | dispatch(...dispatchArgs) 41 | } 42 | } 43 | 44 | return d 45 | } 46 | 47 | export { morphReducer, customDispatcher } 48 | -------------------------------------------------------------------------------- /example/simple-app/actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | export const REQUEST_POSTS = 'REQUEST_POSTS' 4 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 5 | export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT' 6 | export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT' 7 | 8 | export function selectSubreddit(subreddit) { 9 | return { 10 | type: SELECT_SUBREDDIT, 11 | subreddit, 12 | } 13 | } 14 | 15 | export function invalidateSubreddit(subreddit) { 16 | return { 17 | type: INVALIDATE_SUBREDDIT, 18 | subreddit, 19 | } 20 | } 21 | 22 | function requestPosts(subreddit) { 23 | return { 24 | type: REQUEST_POSTS, 25 | subreddit, 26 | } 27 | } 28 | 29 | function receivePosts(subreddit, json) { 30 | return { 31 | type: RECEIVE_POSTS, 32 | subreddit, 33 | posts: json.data.children.map(child => child.data), 34 | receivedAt: Date.now(), 35 | } 36 | } 37 | 38 | function fetchPosts(subreddit) { 39 | return dispatch => { 40 | dispatch(requestPosts(subreddit)) 41 | return fetch(`https://www.reddit.com/r/${subreddit}.json`) 42 | .then(response => response.json()) 43 | .then(json => dispatch(receivePosts(subreddit, json))) 44 | } 45 | } 46 | 47 | function shouldFetchPosts(state, subreddit) { 48 | const posts = state.postsBySubreddit[subreddit] 49 | 50 | if (!posts) { 51 | return true 52 | } 53 | 54 | if (posts.isFetching) { 55 | return false 56 | } 57 | 58 | return posts.didInvalidate 59 | } 60 | 61 | export function fetchPostsIfNeeded(subreddit) { 62 | return (dispatch, getState) => { 63 | if (shouldFetchPosts(getState(), subreddit)) { 64 | return dispatch(fetchPosts(subreddit)) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/simple-app/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Picker = props => { 5 | const { value, onChange, options } = props 6 | 7 | return ( 8 | 9 |

{value}

10 | 17 |
18 | ) 19 | } 20 | 21 | Picker.propTypes = { 22 | options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, 23 | value: PropTypes.string.isRequired, 24 | onChange: PropTypes.func.isRequired, 25 | } 26 | 27 | export default Picker 28 | -------------------------------------------------------------------------------- /example/simple-app/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Posts = props => ( 5 | 10 | ) 11 | 12 | Posts.propTypes = { 13 | posts: PropTypes.array.isRequired, 14 | } 15 | 16 | export default Posts 17 | -------------------------------------------------------------------------------- /example/simple-app/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'storeonize/react-redux' 4 | import { 5 | selectSubreddit, 6 | fetchPostsIfNeeded, 7 | invalidateSubreddit, 8 | } from '../actions' 9 | import Picker from '../components/Picker' 10 | import Posts from '../components/Posts' 11 | 12 | class AsyncApp extends Component { 13 | componentDidMount() { 14 | const { selectedSubreddit, fetchPostsIfNeeded } = this.props 15 | 16 | fetchPostsIfNeeded(selectedSubreddit) 17 | } 18 | 19 | componentDidUpdate(prevProps) { 20 | if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) { 21 | const { selectedSubreddit, fetchPostsIfNeeded } = this.props 22 | 23 | fetchPostsIfNeeded(selectedSubreddit) 24 | } 25 | } 26 | 27 | handleChange = nextSubreddit => { 28 | const { 29 | selectedSubreddit, 30 | fetchPostsIfNeeded, 31 | selectSubreddit, 32 | } = this.props 33 | 34 | selectSubreddit(nextSubreddit) 35 | fetchPostsIfNeeded(selectedSubreddit) 36 | } 37 | 38 | handleRefreshClick = e => { 39 | e.preventDefault() 40 | 41 | const { 42 | selectedSubreddit, 43 | fetchPostsIfNeeded, 44 | invalidateSubreddit, 45 | } = this.props 46 | 47 | invalidateSubreddit(selectedSubreddit) 48 | fetchPostsIfNeeded(selectedSubreddit) 49 | } 50 | 51 | render() { 52 | const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props 53 | 54 | return ( 55 |
56 | 61 |

62 | {lastUpdated && ( 63 | 64 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '} 65 | 66 | )} 67 |

68 | {isFetching && posts.length === 0 &&

Loading...

} 69 | {!isFetching && posts.length === 0 &&

Empty.

} 70 | {posts.length > 0 && ( 71 |
72 | 73 |
74 | )} 75 |
76 | ) 77 | } 78 | } 79 | 80 | AsyncApp.propTypes = { 81 | selectedSubreddit: PropTypes.string.isRequired, 82 | posts: PropTypes.array.isRequired, 83 | isFetching: PropTypes.bool.isRequired, 84 | lastUpdated: PropTypes.number, 85 | fetchPostsIfNeeded: PropTypes.func.isRequired, 86 | selectSubreddit: PropTypes.func.isRequired, 87 | } 88 | 89 | const mapStateToProps = state => { 90 | const { selectedSubreddit, postsBySubreddit } = state 91 | const { isFetching, lastUpdated, items: posts } = postsBySubreddit[ 92 | selectedSubreddit 93 | ] || { 94 | isFetching: true, 95 | items: [], 96 | } 97 | 98 | return { 99 | selectedSubreddit, 100 | posts, 101 | isFetching, 102 | lastUpdated, 103 | } 104 | } 105 | 106 | const mergeProps = (stateProps, { dispatch }) => { 107 | return { 108 | ...stateProps, 109 | fetchPostsIfNeeded: subreddit => dispatch(fetchPostsIfNeeded(subreddit)), 110 | selectSubreddit: subreddit => dispatch(selectSubreddit(subreddit)), 111 | invalidateSubreddit: subreddit => dispatch(invalidateSubreddit(subreddit)), 112 | } 113 | } 114 | 115 | export default connect(mapStateToProps, null, mergeProps)(AsyncApp) 116 | -------------------------------------------------------------------------------- /example/simple-app/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense, lazy } from 'react' 2 | import { Provider } from 'storeonize/react-redux' 3 | import configureStore from '../store' 4 | 5 | const store = configureStore() 6 | 7 | const LazyApp = lazy(() => import('./App')) 8 | 9 | export default () => ( 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /example/simple-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redux 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/simple-app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import Root from './containers/Root' 4 | 5 | render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /example/simple-app/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'storeonize/redux' 2 | import { 3 | SELECT_SUBREDDIT, 4 | INVALIDATE_SUBREDDIT, 5 | REQUEST_POSTS, 6 | RECEIVE_POSTS, 7 | } from './actions' 8 | 9 | function selectedSubreddit(state = 'reactjs', action) { 10 | const { type } = action 11 | 12 | if (type === SELECT_SUBREDDIT) { 13 | return action.subreddit 14 | } 15 | 16 | return state 17 | } 18 | 19 | function posts( 20 | state = { 21 | isFetching: false, 22 | didInvalidate: false, 23 | items: [], 24 | }, 25 | action 26 | ) { 27 | const { type } = action 28 | 29 | if (type === INVALIDATE_SUBREDDIT) { 30 | return { 31 | ...state, 32 | didInvalidate: true, 33 | } 34 | } 35 | 36 | if (type === REQUEST_POSTS) { 37 | return { 38 | ...state, 39 | isFetching: true, 40 | didInvalidate: false, 41 | } 42 | } 43 | 44 | if (type === RECEIVE_POSTS) { 45 | return { 46 | ...state, 47 | isFetching: false, 48 | didInvalidate: false, 49 | items: action.posts, 50 | lastUpdated: action.receivedAt, 51 | } 52 | } 53 | 54 | return state 55 | } 56 | 57 | function postsBySubreddit(state = {}, action) { 58 | const { type } = action 59 | 60 | if ( 61 | type === INVALIDATE_SUBREDDIT || 62 | type === RECEIVE_POSTS || 63 | type === REQUEST_POSTS 64 | ) { 65 | return { 66 | ...state, 67 | [action.subreddit]: posts(state[action.subreddit], action), 68 | } 69 | } 70 | 71 | return state 72 | } 73 | 74 | const rootReducer = combineReducers({ 75 | postsBySubreddit: [ 76 | postsBySubreddit, 77 | [INVALIDATE_SUBREDDIT, RECEIVE_POSTS, REQUEST_POSTS], 78 | ], 79 | selectedSubreddit: [selectedSubreddit, [SELECT_SUBREDDIT]], 80 | }) 81 | 82 | export default rootReducer 83 | -------------------------------------------------------------------------------- /example/simple-app/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'storeonize/redux' 2 | import thunkMiddleware from 'storeonize/redux-thunk' 3 | import { createLogger } from 'storeonize/redux-logger' 4 | import { composeWithDevTools } from 'storeonize/redux-devtools-extension' 5 | import rootReducer from './reducers' 6 | 7 | const loggerMiddleware = createLogger() 8 | 9 | export default preloadedState => 10 | createStore( 11 | rootReducer, 12 | preloadedState, 13 | composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware)) 14 | ) 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { StoreContext, useStoreon } = require('storeon/react') 3 | 4 | const Provider = props => ( 5 | 6 | {props.children} 7 | 8 | ) 9 | 10 | const customDispatcher = (...args) => { 11 | console.log(args) 12 | 13 | // TODO 14 | } 15 | 16 | const connect = (mapStateToProps, mapDispatchToProps, mergeProps) => Target => { 17 | const ConnectedComponent = props => { 18 | const { store } = useStoreon('store') 19 | const mstp = mapStateToProps(store) 20 | const mdtp = mapDispatchToProps ? mapDispatchToProps() : {} // TODO 21 | const mp = mergeProps 22 | ? mergeProps(mstp, { dispatch: (...args) => customDispatcher(...args) }) 23 | : mstp 24 | 25 | return ( 26 | <> 27 | 28 | 29 | ) 30 | } 31 | 32 | return ConnectedComponent 33 | } 34 | 35 | module.exports = { Provider, connect } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storeonize", 3 | "version": "1.0.0-alpha4", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "example:simple-app": "parcel example/simple-app/index.html", 8 | "size": "size-limit" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "repository": "octav47/storeonize", 13 | "alias": { 14 | "storeonize": "./" 15 | }, 16 | "peerDependencies": { 17 | "react": "^16.13.1", 18 | "storeon": "^3.0.3" 19 | }, 20 | "devDependencies": { 21 | "@babel/preset-react": "7.10.4", 22 | "@size-limit/preset-small-lib": "4.5.7", 23 | "isomorphic-fetch": "2.2.1", 24 | "parcel-bundler": "1.12.4", 25 | "prettier": "2.1.1", 26 | "prop-types": "latest", 27 | "react-dom": "16.13.1", 28 | "react-redux": "7.2.1", 29 | "redux": "^4.0.5", 30 | "redux-logger": "3.0.6", 31 | "redux-thunk": "2.3.0", 32 | "size-limit": "4.5.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | arrowParens: 'avoid', 7 | } 8 | -------------------------------------------------------------------------------- /react-redux/index.js: -------------------------------------------------------------------------------- 1 | import React, { createElement as h } from 'react' 2 | import { StoreContext, useStoreon } from 'storeon/react' 3 | import { customDispatcher } from '../core' 4 | 5 | const Provider = ({ store, children }) => 6 | h(StoreContext.Provider, { value: store }, children) 7 | 8 | const connect = (mapStateToProps, mapDispatchToProps, mergeProps) => Target => { 9 | return props => { 10 | const { store, dispatch: storeonDispatcher } = useStoreon('store') 11 | const dispatch = customDispatcher(storeonDispatcher, store) 12 | const mapStateToPropsValues = mapStateToProps(store, props) 13 | const mapDispatchToPropsValues = mapDispatchToProps 14 | ? mapDispatchToProps(dispatch, props) 15 | : { dispatch } 16 | const mergePropsValues = mergeProps 17 | ? mergeProps(mapStateToPropsValues, mapDispatchToPropsValues, props) 18 | : null 19 | 20 | if (mergePropsValues) { 21 | return 22 | } 23 | 24 | if (mapDispatchToPropsValues) { 25 | return 26 | } 27 | 28 | return 29 | } 30 | } 31 | 32 | export { Provider, connect } 33 | -------------------------------------------------------------------------------- /redux-devtools-extension/index.js: -------------------------------------------------------------------------------- 1 | import { storeonDevtools } from 'storeon/devtools' 2 | 3 | const composeWithDevTools = applyMiddlewareResult => { 4 | return [ 5 | ...applyMiddlewareResult, 6 | process.env.NODE_ENV !== 'production' && storeonDevtools, 7 | ] 8 | } 9 | 10 | export { composeWithDevTools } 11 | -------------------------------------------------------------------------------- /redux-logger/index.js: -------------------------------------------------------------------------------- 1 | import { storeonLogger } from 'storeon/devtools' 2 | 3 | const createLogger = () => storeonLogger 4 | 5 | export { createLogger } 6 | -------------------------------------------------------------------------------- /redux-thunk/index.js: -------------------------------------------------------------------------------- 1 | const thunkMiddleware = () => { 2 | // ignore this, because customDispatcher from core can handle thunk-like actions 3 | } 4 | 5 | export default thunkMiddleware 6 | -------------------------------------------------------------------------------- /redux/index.js: -------------------------------------------------------------------------------- 1 | import { createStoreon } from 'storeon' 2 | 3 | import { morphReducer } from '../core' 4 | 5 | const combineReducers = reducersMap => { 6 | const reducersKeys = Object.keys(reducersMap) 7 | 8 | return reducersKeys.map(reducerName => { 9 | const [reducer, actions] = reducersMap[reducerName] 10 | 11 | return morphReducer(reducerName, reducer, actions) 12 | }) 13 | } 14 | 15 | const createStore = (reducer, preloadedState = {}, middleware = []) => { 16 | const preloadedStateModule = store => { 17 | Object.keys(preloadedState).forEach(key => { 18 | store.on('@init', s => { 19 | return { 20 | store: { 21 | ...s.store, 22 | [key]: preloadedState[key], 23 | }, 24 | } 25 | }) 26 | }) 27 | } 28 | 29 | return createStoreon([preloadedStateModule, ...reducer, ...middleware]) 30 | } 31 | 32 | const applyMiddleware = (...args) => args 33 | 34 | export { combineReducers, createStore, applyMiddleware } 35 | --------------------------------------------------------------------------------