feat: redux, entities
This commit is contained in:
parent
44d76e1ca9
commit
2de1665426
|
@ -36,11 +36,13 @@
|
|||
"html-entities": "1.3.1",
|
||||
"immer": "^7.0.1",
|
||||
"memorystore": "^1.6.2",
|
||||
"normalizr": "^3.6.0",
|
||||
"prop-types": "^15",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-tabs": "^3.1.1",
|
||||
"scwp": "1.x",
|
||||
"socket.io-redis": "^5.3.0",
|
||||
|
|
|
@ -16,7 +16,7 @@ const App = () => {
|
|||
const [typeRenderers] = useState(typeRenderMap());
|
||||
return (
|
||||
<TypeRenderersContext.Provider value={typeRenderers}>
|
||||
<Resource uri="/mama-kitty.entity.json" />
|
||||
<Resource uri="/resources/cha0s/initial/kitty.entity.json" />
|
||||
</TypeRenderersContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,9 +3,18 @@ import './index.scss';
|
|||
import {enableMapSet} from 'immer';
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import App from './app';
|
||||
import createStore from './store';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
render(<App />, document.getElementById('root'));
|
||||
render(
|
||||
(
|
||||
<Provider store={createStore()}>
|
||||
<App />
|
||||
</Provider>
|
||||
),
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
import {compose} from '@avocado/core';
|
||||
import {Entity} from '@avocado/entity';
|
||||
import contempo from 'contempo';
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {fetchEntity, selectEntityByUri} from './state';
|
||||
import Traits from './traits';
|
||||
|
||||
const decorate = compose(
|
||||
contempo(require('./index.raw.scss')),
|
||||
contempo(require('./entity.raw.scss')),
|
||||
);
|
||||
|
||||
const json = require('~/../fixtures/rock-projectile.entity.json');
|
||||
|
||||
const EntityComponent = (props) => {
|
||||
const {uri} = props;
|
||||
const dispatch = useDispatch();
|
||||
const json = useSelector((state) => selectEntityByUri(state, uri));
|
||||
useEffect(() => {
|
||||
if (!json) {
|
||||
dispatch(fetchEntity(uri));
|
||||
}
|
||||
}, [json, uri]);
|
||||
if (!json) {
|
||||
return null;
|
||||
}
|
||||
const entity = new Entity(json);
|
||||
const {traits} = json;
|
||||
return (
|
|
@ -1,6 +1,8 @@
|
|||
import EntityComponent from './component';
|
||||
import EntityComponent from './entity';
|
||||
|
||||
export default {
|
||||
keys: ['entity', 'entities'],
|
||||
matcher: /\.entity\.json$/,
|
||||
Component: EntityComponent,
|
||||
state: require('./state'),
|
||||
};
|
||||
|
|
59
src/client/resources/entity/state.js
Normal file
59
src/client/resources/entity/state.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {normalize, schema} from 'normalizr';
|
||||
import {
|
||||
createAsyncThunk,
|
||||
createEntityAdapter,
|
||||
createSelector,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit';
|
||||
|
||||
const entitySchema = new schema.Entity('entities', {}, {idAttribute: 'uuid'});
|
||||
|
||||
export const fetchEntity = createAsyncThunk(
|
||||
'entities/fetchEntity',
|
||||
async (uri) => {
|
||||
const data = await fetch(uri);
|
||||
const json = await data.json();
|
||||
const {entities} = normalize(json, entitySchema);
|
||||
return entities;
|
||||
}
|
||||
);
|
||||
|
||||
const adapter = createEntityAdapter({
|
||||
selectId: (entity) => entity.uuid,
|
||||
sortComparer: (a, b) => a.uuid.localeCompare(b.uuid),
|
||||
});
|
||||
|
||||
export const {
|
||||
selectById: selectEntityById,
|
||||
selectIds: selectEntityIds,
|
||||
selectEntities: selectEntityEntities,
|
||||
selectAll: selectAllEntities,
|
||||
selectTotal: selectTotalEntities
|
||||
} = adapter.getSelectors((state) => state.entities)
|
||||
|
||||
export const selectEntityUris = createSelector(
|
||||
(state) => state.entities,
|
||||
(entities) => entities.uris,
|
||||
);
|
||||
|
||||
export const selectEntityByUri = createSelector(
|
||||
(state) => state.entities,
|
||||
selectEntityUris,
|
||||
(_, uri) => uri,
|
||||
({entities}, uris, uri) => entities[uris[uri]],
|
||||
)
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'entities',
|
||||
initialState: adapter.getInitialState({uris: {}}),
|
||||
reducers: {},
|
||||
extraReducers: {
|
||||
[fetchEntity.fulfilled]: (state, action) => {
|
||||
adapter.upsertMany(state, action.payload.entities);
|
||||
Object.entries(action.payload.entities)
|
||||
.forEach(([id, entity]) => state.uris[entity.uri] = id);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default slice.reducer;
|
25
src/client/store.js
Normal file
25
src/client/store.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import merge from 'deepmerge';
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import createCommonStore from '~/common/store';
|
||||
|
||||
import {all} from './resources.scwp';
|
||||
|
||||
const resources = Object.values(all()).map((M) => M.default);
|
||||
const reducerMap = resources.reduce(
|
||||
(r, {keys, state: {'default': reducer}}) => ({...r, [keys[1]]: reducer}),
|
||||
{}
|
||||
);
|
||||
|
||||
export default function createStore(options = {}) {
|
||||
return createCommonStore(
|
||||
merge(
|
||||
options,
|
||||
{
|
||||
middleware: [],
|
||||
reducer: combineReducers(reducerMap),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
2
src/common/store/effects.js
vendored
Normal file
2
src/common/store/effects.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
export default {
|
||||
};
|
18
src/common/store/index.js
Normal file
18
src/common/store/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import merge from 'deepmerge';
|
||||
import {configureStore, getDefaultMiddleware} from '@reduxjs/toolkit';
|
||||
|
||||
import commonMiddleware from './middleware';
|
||||
|
||||
export default function createStore(options = {}) {
|
||||
return configureStore(
|
||||
merge(
|
||||
{
|
||||
middleware: [
|
||||
...getDefaultMiddleware(),
|
||||
commonMiddleware,
|
||||
],
|
||||
},
|
||||
options,
|
||||
),
|
||||
);
|
||||
}
|
16
src/common/store/middleware.js
Normal file
16
src/common/store/middleware.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import effects from './effects';
|
||||
|
||||
const debug = require('debug')('persea:common:store');
|
||||
|
||||
export default (store) => (next) => (action) => {
|
||||
const {meta, payload} = action;
|
||||
debug("action '%s' dispatched: %o", action.type, {
|
||||
payload,
|
||||
meta,
|
||||
});
|
||||
const result = next(action);
|
||||
if (effects[action.type]) {
|
||||
setTimeout(() => effects[action.type](store, action), 0);
|
||||
}
|
||||
return result;
|
||||
};
|
|
@ -1,6 +1,8 @@
|
|||
import express from 'express';
|
||||
import httpSession from 'express-session';
|
||||
|
||||
import createRoutes from './routes/create';
|
||||
|
||||
const MemoryStore = require('memorystore')(httpSession);
|
||||
|
||||
let insideSession;
|
||||
|
@ -19,6 +21,7 @@ export default function createApp() {
|
|||
}),
|
||||
});
|
||||
app.use(insideSession);
|
||||
createRoutes(app);
|
||||
return app;
|
||||
}
|
||||
|
||||
|
|
5
src/server/routes/create.js
Normal file
5
src/server/routes/create.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import express from 'express';
|
||||
|
||||
export default function createRoutes(app) {
|
||||
app.use('/resources', express.static('resources'));
|
||||
}
|
25
yarn.lock
25
yarn.lock
|
@ -989,6 +989,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.5.5":
|
||||
version "7.10.3"
|
||||
resolved "https://npm.i12e.cha0s.io/@babel%2fruntime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
|
||||
integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.1":
|
||||
version "7.10.1"
|
||||
resolved "https://npm.i12e.cha0s.io/@babel%2ftemplate/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
|
||||
|
@ -6598,6 +6605,11 @@ normalize-url@1.9.1:
|
|||
query-string "^4.1.0"
|
||||
sort-keys "^1.0.0"
|
||||
|
||||
normalizr@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://npm.i12e.cha0s.io/normalizr/-/normalizr-3.6.0.tgz#b8bbc4546ffe43c1c2200503041642915fcd3e1c"
|
||||
integrity sha512-25cd8DiDu+pL46KIaxtVVvvEPjGacJgv0yUg950evr62dQ/ks2JO1kf7+Vi5/rMFjaSTSTls7aCnmRlUSljtiA==
|
||||
|
||||
notepack.io@~2.1.2:
|
||||
version "2.1.3"
|
||||
resolved "https://npm.i12e.cha0s.io/notepack.io/-/notepack.io-2.1.3.tgz#cc904045c751b1a27b2dcfd838d81d0bf3ced923"
|
||||
|
@ -7601,7 +7613,7 @@ react-hot-loader@^4.12.21:
|
|||
shallowequal "^1.1.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
|
||||
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0:
|
||||
version "16.13.1"
|
||||
resolved "https://npm.i12e.cha0s.io/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
@ -7625,6 +7637,17 @@ react-markdown@^4.3.1:
|
|||
unist-util-visit "^1.3.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
react-redux@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://npm.i12e.cha0s.io/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
|
||||
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-tabs@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://npm.i12e.cha0s.io/react-tabs/-/react-tabs-3.1.1.tgz#b363a239f76046bb2158875a1e5921b11064052f"
|
||||
|
|
Loading…
Reference in New Issue
Block a user