Skip to content

Instantly share code, notes, and snippets.

@jcppman
Last active December 3, 2022 02:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jcppman/7d82853ea6be03855bf8b961ea1a9770 to your computer and use it in GitHub Desktop.
Save jcppman/7d82853ea6be03855bf8b961ea1a9770 to your computer and use it in GitHub Desktop.
Example
/* some helpers */
import { batch } from 'redux-act';
import { createStore, combineReducers } from 'redux';
import { get, isFunction, forEach, mapValues } from 'lodash';
import { Observable } from 'rxjs';
// create a store, combine all submodules to the store
// and bind all actions and the batch method to the created store
export const createAndBindStore = (modules, highOrderReducer) => {
let reducer = combineReducers(mapValues(modules, m => m.reducer));
if (highOrderReducer) {
reducer = highOrderReducer(reducer);
}
const store = createStore(reducer);
batch.assignTo(store);
forEach(modules, ({ actions, init }, moduleName) => {
forEach(actions, val => {
if (val.isMeta) {
// this assignTo is actually more like setGetState
// but name it assignTo to keep it consistent with regular
// redux-act actions
val.assignTo(() => get(store.getState(), moduleName));
return;
}
val.assignTo(store);
});
if (isFunction(init)) {
init(Observable.from(store).map(s => s[moduleName]));
}
});
return store;
};
// wrap the given function, provide it the ability to get current store state
// by `getState`
// createMataAction((getState, payload) => {});
export const createMetaAction = actionBody => {
let getState;
const asyncAction = (...args) => actionBody(getState, ...args);
asyncAction.assignTo = _getState => {
getState = _getState;
};
asyncAction.isMeta = true;
return asyncAction;
};
export default { createAndBindStore, createMetaAction };
import { createAction, createReducer, batch } from 'redux-act';
import produce from 'immer';
import { createMetaAction } from './redux-utils';
// a service getter/setter which uses observable, getItems$ returns a lazy executed observable, only fire the
// request when subscribed
import { getItems$, saveItems$ } from 'items-service';
const addTodo = createAction();
const removeTodo = createAction();
const editTodo = createAction();
const setItems = createAction();
const setLoading = createAction();
const importItems = createMetaAction(async (getState, url) => {
setLoading(false);
// get items from an url
const items = await fetch(url);
batch(
setLoading.raw(true),
setItems.raw(items),
);
});
export const actions = {
setItems,
addTodo,
removeTodo,
editTodo,
setLoading,
importItems,
};
export const reducer = createReducer({
[setLoading]: (state, isLoading) =>
produce(function () {
this.isLoading = isLoading;
}),
[addTodo]: (state, content) =>
produce(state, function() {
this.items.push({
content,
isDone: false,
timestamp: new Date().getTime(),
});
}),
[removeTodo]: (state, index) =>
produce(state, function() {
this.items.splice(index, 1);
}),
[editTodo]: (state, payload) =>
produce(state, function() {
const { index } = payload;
this.items[index] = {
...this.items[index],
...payload,
};
}),
[setItems]: (state, items) =>
produce(state, function() {
this.items = items;
}),
}, {
items: [],
isLoading: false,
});
export const init = (store$) => {
setLoading(true);
const loadItems = getItems$().do((items) => {
batch(
// batch actions, worth noting that because actions
// are binded to the store, we need the `raw` version
// for batch
setItems.raw(items),
setLoading.raw(false),
)
});
// store$ is an observable so we can utilize all kind of its fany operators
store$
// don't do anything before the initial items from the backend is loaded
// and set to the store, otherwise we'll end up feeding the items backend
// just gave us back to the server
.skipUntil(loadItems)
// in terms of saving to backend, all we care is the property `items`
.pluck('items')
// a little bit taste of the rx power
.debounceTime(1000)
.switchMap(items =>
saveItems$(items).retry(3)
).subscribe();
};
export default {
actions,
reducer,
init,
};
import { Component } from 'react';
import { withState, compose } from 'recompose';
import withTodos from './withTodos';
// let's assume these components exist
import TodoItem from './components/TodoItem';
import AddButton from './components/AddButton';
import InputBox from './components/InputBox';
// if in the future we need it elsewhere, maybe promote it to a global state
// but at this point it's totally ok to use local state in this scenario
// and here we use recompose to make our lifes easier
const enhance = compose(
withTodos(),
withState('textValue', 'setValue', ''),
);
class TodoList extends Component {
handleAddTodo() {
const {
textValue,
setValue,
addTodo,
} = this.props;
addTodo(textValue);
setValue('');
}
handleContentChange(idx, content) {
const { editTodo } = this.props;
editTodo(idx, { content });
}
handleDoneChanged(idx) {
const { items, editTodo } = this.props;
editTodo(idx, { isDone: !items[idx].isDone });
}
render() {
const {
tasks,
addTodo,
removeTodo,
editTodo,
textValue,
setValue,
} = props;
const {
handleContentChange,
handleDoneChanged,
handleAddTodo,
} = this;
return (
<div>
{tasks.map((t, idx) =>
<TodoItem
item={t}
key={t.wtv}
onContentChanged={handleContentChange}
onDoneChanged={handleDoneChanged}
/>
)}
<InputBox onChange={setValue} value={textValue} />
<AddButton onClick={handlAddTodo} />
</div>
);
}
};
export default enhance(TodoList);
// a Store Connector
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { actions } from './state';
const getUndone = createSelector(
state => state.items,
items => items.filter(i => !i.isDone),
);
const withTodos = () => connect(
state => ({
undoneTasks: getUndone(state),
tasks: state.items,
}),
() => (actions)
);
export default withTodos;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment