Skip to content

Commit

Permalink
support sharing redux store for a request (#1518)
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip authored Feb 1, 2020
1 parent 4b8c4b2 commit a37d03a
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 39 deletions.
9 changes: 8 additions & 1 deletion packages/subapp-react/lib/framework-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ class FrameworkLib {
reduxData = await prepare({ request, context });
}

let storeContainer = context.user.storeContainer;

if (!storeContainer) {
storeContainer = context.user.storeContainer = {};
}

if (!reduxData) {
reduxData = { initialState: {} };
}
Expand All @@ -154,7 +160,8 @@ class FrameworkLib {
// next we take the initial state and create redux store from it
this.store =
reduxData.store ||
(subApp.reduxCreateStore && (await subApp.reduxCreateStore(this.initialState)));
(subApp.reduxCreateStore &&
(await subApp.reduxCreateStore(this.initialState, storeContainer)));
assert(
this.store,
`redux subapp ${subApp.name} didn't provide store, reduxCreateStore, or reducers`
Expand Down
55 changes: 35 additions & 20 deletions packages/subapp-react/test/spec/ssr-framework.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,30 +189,45 @@ describe("SSR React framework", function() {
expect(res).contains("Hello foo bar");
});

it("should init redux store and render Component", async () => {
it("should init redux store in context and render Component", async () => {
const Component = connect(x => x)(props => <div>Hello {props.test}</div>);
let storeReady;

const framework = new lib.FrameworkLib({
subApp: {
__redux: true,
Component,
reduxCreateStore: initState => Redux.createStore(x => x, initState),
async reduxStoreReady() {
storeReady = true;
},
prepare: () => ({ test: "foo bar" })
const subApp = {
__redux: true,
Component,
reduxCreateStore: (initState, container) => {
// simulate sharing store in conainter (see subapp-redux/lib/shared)
if (!container.store) {
container.store = Redux.createStore(x => x, initState);
}
return container.store;
},
subAppServer: {},
options: { serverSideRendering: true },
context: {
user: {}
}
});
const res = await framework.handleSSR();
expect(res).contains("Hello foo bar");
expect(framework.initialStateStr).equals(`{"test":"foo bar"}`);
expect(storeReady).equal(true);
async reduxStoreReady() {
storeReady = true;
},

prepare: () => ({ test: "foo bar" })
};
const context = { user: {} };
const verify = async () => {
const framework = new lib.FrameworkLib({
subApp,
subAppServer: {},
options: { serverSideRendering: true },
context
});
const res = await framework.handleSSR();
expect(res).contains("Hello foo bar");
expect(framework.initialStateStr).equals(`{"test":"foo bar"}`);
expect(context.user).to.have.property("storeContainer");
expect(storeReady).equal(true);
};
await verify();
const store = context.user.storeContainer.store;
// should be able to render again with the same store in context
await verify();
expect(store).to.equal(context.user.storeContainer.store);
});

it("should init redux store and render Component but doesn't attach initial state", async () => {
Expand Down
54 changes: 36 additions & 18 deletions packages/subapp-redux/src/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,33 @@ import { createStore, combineReducers } from "redux";
// - a top level initializer can be specified to do this
//

let shared = { namedStores: {} };
let persistenStoreContainer = { namedStores: {} };

function setStoreContainer(container) {
shared = container;
shared.namedStores = shared.namedStores || {};
function initContainer(storeContainer) {
storeContainer = storeContainer || persistenStoreContainer;
if (!storeContainer.namedStores) {
storeContainer.namedStores = {};
}
return storeContainer;
}

function setStoreContainer(storeContainer) {
persistenStoreContainer = storeContainer;
initContainer(storeContainer);
}

function clearSharedStore() {
shared.namedStores = {};
persistenStoreContainer.namedStores = {};
}

function getSharedStore(name) {
return (name && shared.namedStores[name === true ? "_" : name]) || {};
function getSharedStore(name, storeContainer) {
storeContainer = initContainer(storeContainer);
return (name && storeContainer.namedStores[name === true ? "_" : name]) || {};
}

function setSharedStore(name, contents) {
shared.namedStores[name === true ? "_" : name] = contents;
function setSharedStore(name, contents, storeContainer) {
storeContainer = initContainer(storeContainer);
storeContainer.namedStores[name === true ? "_" : name] = contents;
}

const assert = (flag, msg) => {
Expand Down Expand Up @@ -64,21 +74,21 @@ const combineSharedReducers = (info, container, reducers) => {
return combineReducers(addSharedReducer(info, container, reducers));
};

function replaceReducer(newReducers, info) {
let { store, reducerContainer } = getSharedStore(info.reduxShareStore);
function replaceReducer(newReducers, info, storeContainer) {
let { store, reducerContainer } = getSharedStore(info.reduxShareStore, storeContainer);
const reducer = combineSharedReducers(info, reducerContainer, newReducers);
return store[originalReplaceReducerSym](reducer);
}

function createSharedStore(initialState, info) {
const sharedStore = info.reduxShareStore;
function createSharedStore(initialState, info, storeContainer) {
const sharedStoreName = info.reduxShareStore;

if (sharedStore) {
if (sharedStoreName) {
assert(
info._genReduxCreateStore || !info.reduxCreateStore,
`${WHEN_SHARED_MSG}, you cannot have reduxCreateStore`
);
let { store, reducerContainer } = getSharedStore(sharedStore);
let { store, reducerContainer } = getSharedStore(sharedStoreName, storeContainer);
if (store) {
// TODO: redux doesn't have a way to set initial state
// after store's created? What can we do about this?
Expand All @@ -97,14 +107,22 @@ function createSharedStore(initialState, info) {
// that alters argument type is worst
// Maybe create a proxy store object, one for each sub-app
//
store.replaceReducer = (reducers, info2) => replaceReducer(reducers, info2);
// NOTE: It's only on SSR that we need to share store within the
// request and replaceReducer doesn't make sense for SSR, but it's
// taking a storeContainer here anyways.
//
store.replaceReducer = (reducers, info2, storeContainer2) => {
return replaceReducer(reducers, info2, storeContainer2);
};
}
setSharedStore(sharedStore, { store, reducerContainer });
setSharedStore(sharedStoreName, { store, reducerContainer }, storeContainer);
return store;
}

// call user provided reduxCreateStore
if (info.reduxCreateStore && !info._genReduxCreateStore) {
// TODO: given the complexities of dealing with and maintaining store
// allowing user reduxCreateStore is not a good idea. Consider for removal.
return info.reduxCreateStore(initialState);
}

Expand All @@ -121,7 +139,7 @@ function createSharedStore(initialState, info) {
}

function getReduxCreateStore(info) {
return initialState => createSharedStore(initialState, info);
return (initialState, storeContainer) => createSharedStore(initialState, info, storeContainer);
}

export {
Expand Down

0 comments on commit a37d03a

Please sign in to comment.