├── .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 |
6 | {props.posts.map((post, i) => (
7 | - {post.title}
8 | ))}
9 |
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 |
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 |
--------------------------------------------------------------------------------