Letās see if I can do a quick summary of our API options for computations (or at least the ones weāve come up with so far).
React Computations
connect
This is one is working perfectly and easy to use:
const Comp = ({ state, actions, libraries }) => {
return (
// use state, actions and libraries in your component...
);
};
export default connect(Comp);
I donāt see the need to change it or add an additional API.
State Computations
By state computations I mean ways to subscribe to state
mutations.
I want this code to be executed when this part of the state
changes.
observe
(from react-easy-state)
This is the one that we have right now, inherited from react-easy-state
. It is not documented, so we can deprecate it if needed:
observe(() => {
actions.analytics.sendPageview(state.router.link);
});
One modification could be to pass proxified state and actions to the callback:
observe(({ state, actions }) => {
actions.analytics.sendPageview(state.router.link);
});
The main advantage of passing the state to the computation is that we can use that information in the devtools.
reaction
(from mobx)
It could be without passing state and actions:
reaction(
() => state.router.link,
() => actions.analytics.sendPageview(state.router.link)
);
It could pass the observed (returned) value. This is actually how it works in Mobx.
reaction(
() => state.router.link,
link => actions.analytics.sendPageview(link)
);
It could get state and actions in the calbacks:
reaction(
({ state }) => state.router.link,
({ state, actions }) => {
actions.analytics.sendPageview(state.router.link);
}
);
It could be an object instead of two functions and accept more params, like name or priority:
reaction({
name: "triggerPageviews",
test: ({ state }) => state.router.link,
reaction: ({ state, actions }) => {
actions.analytics.sendPageview(state.router.link);
}
});
The main advantage of having a name is that we can use that in the devtools.
The test function
The test function (whether it lives in an object or not) can be a computation like this:
reaction({
test: ({ state }) => state.router.link,
});
a function that gets the path and/or patch:
reaction({
test: ({ path }) => path === "state.router.link",
});
reaction({
test: ({ patch }) => patch.type === "set" && patch.path === "state.router.link",
});
a function that gets the current state and previous state:
reaction({
test: ({ prevState, nextState }) =>
prevState.router.link !== nextState.router.link,
});
or any combination of those.
Where do obseve
or reaction
could live?
The observe/reaction functions could live inside actions:
const triggerPageviews = ({ state, actions }) => {
observe(() => {
actions.analytics.sendPageview(state.router.link);
});
};
const triggerPageviews = ({ state, actions }) => {
reaction(
() => state.router.link,
() => actions.analytics.sendPageview(state.router.link)
);
};
and uses would have the option to call that action whenever they see fit, for example, in the init
action:
export default {
state: { ... },
actions: {
analytics: {
triggerPageviews,
init: ({ actions }) => actions.analytics.triggerPageviews()
}
},
libraries: { ... }
}
or initialized when Frontity loads:
- in an array exposed by the packages:
export default {
state: { ... },
actions: { ... },
libraries: { ... },
reactions: [
triggerPageviews
]
}
- in an object with namespaces exposed by the packages:
export default {
state: { ... },
actions: { ... },
libraries: { ... },
reactions: {
analytics: {
triggerPageviews
}
}
}
waitFor
(similar to when
in mobx or take
in redux-saga)
This promise could resolve when the state changes.
const triggerPageviews = async ({ state, actions }) => {
while(true) {
await waitFor(() => state.router.link);
actions.analytics.sendPageview(state.router.link);
}
};
It could return the part of the state that changed.
const triggerPageviews = async ({ state, actions }) => {
while(true) {
const link = await waitFor(() => state.router.link);
actions.analytics.sendPageview(link);
}
};
It could get the state in the callback.
const triggerPageviews = async ({ state, actions }) => {
while(true) {
const link = await waitFor(({ state }) => state.router.link);
actions.analytics.sendPageview(link);
}
};
Or we could get waitFor
from the action, with a proper parent = triggerPageviews
in the context.
const triggerPageviews = async ({ state, actions, waitFor }) => {
while(true) {
const link = await waitFor(({ state }) => state.router.link);
actions.analytics.sendPageview(link);
}
};
If we do this, it would be fairly easy to show in the devtools that the action triggerPageviews
is being executed but has not finished and it is āwaiting forā a change in state.router.link
.
It could use the prevState
and nextState
syntax:
const triggerPageviews = async ({ state, actions }) => {
while(true) {
await waitFor(
({ prevState, nextState }) => prevState.router.link !== nextState.router.link
);
actions.analytics.sendPageview(state.router.link);
}
};
Action Computations
By action computations I mean ways to subscribe to action
executions.
I want this code to be executed when this action
is executed.
reaction
(like mobx)
It could be without passing state and actions:
reaction(
() => actions.router.set,
() => actions.analytics.sendPageview(state.router.link)
);
It could get state and actions in the callbacks:
reaction(
({ actions }) => actions.router.set,
({ state, actions }) => {
actions.analytics.sendPageview(state.router.link);
}
);
It could be an object instead of two functions and accept more params, like name or priority:
reaction({
name: "triggerPageviews",
priority: 10,
test: ({ actions }) => actions.router.set,
reaction: ({ state, actions }) => {
actions.analytics.sendPageview(state.router.link);
}
});
The test function
The test function (whether it lives in an object or not) can be a computation like this:
reaction({
test: ({ actions }) => actions.router.set,
});
a function that gets the path:
reaction({
test: ({ path }) => path === "actions.router.set",
});
or any combination of those.
Where do reaction
could live?
The reaction functions could live inside other actions:
const triggerPageviews = ({ state, actions }) => {
reaction(
() => actions.router.set,
() => actions.analytics.sendPageview(state.router.link)
);
};
and uses would have the option to call that action whenever they see fit, for example, in the init
action:
export default {
state: { ... },
actions: {
analytics: {
triggerPageviews,
init: ({ actions }) => actions.analytics.triggerPageviews()
}
},
libraries: { ... }
}
or initialized when Frontity loads:
- in an array exposed by the packages:
export default {
state: { ... },
actions: { ... },
libraries: { ... },
reactions: [
triggerPageviews
]
}
- in an object with namespaces exposed by the packages:
export default {
state: { ... },
actions: { ... },
libraries: { ... },
reactions: {
analytics: {
triggerPageviews
}
}
}
waitFor
(similar to when
in mobx or take
in redux-saga)
This promise could resolve when the action is executed changes.
const triggerPageviews = async ({ state, actions }) => {
while(true) {
await waitFor(() => actions.router.set);
actions.analytics.sendPageview(state.router.link);
}
};
It could return the array of arguments of the action that was executed.
const triggerPageviews = async ({ state, actions }) => {
while(true) {
const [link] = await waitFor(() => actions.router.set);
actions.analytics.sendPageview(link);
}
};
It could get the actions in the callback.
const triggerPageviews = async ({ state, actions }) => {
while(true) {
const [link] = await waitFor(({ actions }) => actions.router.set);
actions.analytics.sendPageview(link);
}
};
Or we could get waitFor
from the action, with a proper parent = triggerPageviews
in the context.
const triggerPageviews = async ({ state, actions, waitFor }) => {
while(true) {
const [link] = await waitFor(({ actions }) => actions.router.set);
actions.analytics.sendPageview(link);
}
};
If we do this, it would be fairly easy to show in the devtools that the action triggerPageviews
is being executed but has not finished and it is āwaiting forā an execution of the action actions.router.set
.
Iāll do a similar summary for the middleware APIs.