A document explaining the fundamentals of transitioning a Flux architecture to a Redux architecture.
NOTE: Before attempting to transition, make sure you understand the content from the general Redux Documentation and the OverReact Redux Documentation. This guide covers differences between Flux to Redux but does not try to comprehensively describe Redux.
The goal of this document is explain major elements of transitioning from Flux to Redux. This includes explanation of both a simple and more advanced conversion, and the introduction of a new architecture as a last resort for the most extreme cases.
If, through this process, the document or examples miss any edge cases you encounter, please create an issue or reach out so it can be added.
No. OverReact Redux is meant to help to provide a recommended state management method for OverReact components, as well as provide benefits over w_flux and built_redux. Those benefits may provide enough reason to make a refactor worthwhile. If, after evaluating the benefits and effort, the juice doesn't seem worth the squeeze, then no need to worry about it!
To evaluate if the refactor is worth it, the details of OverReact Redux can be found in the OverReact Redux documentation. That document paired with this guide will illustrate the full scope of costs and benefits.
To illustrate the basic changes that will occur, this section will go through a basic Flux architecture and then show that same system with Redux instead. This section will also have a step by step list of instructions for an update as straightforward as this one. An actual working example can be found in the Flux to Redux Example.
NOTE: This section does not cover the complexities of having multiple stores, which presents more challenges than a single store. For a more complex example see the advanced example below.
For our example, here are our different items of interest:
- The Flux Store Class,
RandomColorStore
- The Actions Class,
RandomColorActions
- The instances of these classes,
randomColorStore
andrandomColorActions
- The Store Object: Our store, called
randomColorStore
, is instantiated withrandomColorActions
being passed in. - The UI: The only component is a BigBlock component that receives randomColorActions and randomColorStore as props. Updates are triggered by calling
props.actions.changeBackgroundColor
and state is accessed by usingprops.store.state
. The actual UI itself is just the component, with its background color set to the state background color, and a button that triggers the action to update the state background color.
import 'package:w_flux/w_flux.dart' as flux;
// A flux store responsible for displaying a random background color.
class RandomColorStore extends flux.Store {
/// The collection of actions this store will be subscribing to.
RandomColorActions _actions;
/// Public data
String _backgroundColor = 'gray';
String get backgroundColor => _backgroundColor;
RandomColorStore(this._actions) {
// Subscribe to an action. When the action fires:
// 1. call _changeBackgroundColor
// 2. trigger an update
triggerOnActionV2(_actions.changeBackgroundColor, _changeBackgroundColor);
}
_changeBackgroundColor(String _) {
// Update the state, which will be reflected in the public getter
_backgroundColor = '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16);
}
}
import 'package:w_flux/w_flux.dart' as flux;
/// A collection of actions.
class RandomColorActions {
/// An action instance that can be used to dispatch events to subscribers.
final changeBackgroundColor = flux.Action<String>();
}
// This same actions object will also be passed into components,
// so that the `Action` instances used to dispatch events are the same ones
// that have been listened to by the store.
final randomColorActions = RandomColorActions();
final randomColorStore = RandomColorStore(randomColorActions);
For our example, here are our different items of interest:
- The State Class,
ReduxState
- Actions, a single action class called
UpdateBackgroundColorAction
- The Reducer,
randomColorReducer
- The Store Object,
randomColorStore
- The UI: All of our UI componentry is wrapped in a
ReduxProvider
. The provider takes in astore
prop, to which we passrandomColorStore
. The only component is aBigBlock
connected component. Updates are triggered by callingprops.dispatch(UpdateBackgroundColorAction())
, and since it's a connected component (usingmapStateToProps
) state is accessed via normal props usage.
// A regular Dart class that represents the application data model
class RandomColorState {
// The state values the app needs
String backgroundColor;
// A constructor used to establish the default state
RandomColorState.defaultState() : this.backgroundColor = 'gray';
// A convenience constructor used by the reducer to return new instances of the class
RandomColorState.withColor(this.backgroundColor);
}
// The only action for our application
class UpdateBackgroundColorAction {}
// A simple reducer that has a case for every action (in this case, a single one)
RandomColorState reducer(RandomColorState oldState, dynamic action) {
if (action is UpdateBackgroundColorAction) {
var color = '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16);
// Return a new state class instance, changing the background color
return RandomColorState.withColor(color);
}
return oldState;
}
import 'package:redux/redux.dart' as redux;
// Instantiate a store using the reducer and state class
redux.Store randomColorStore = redux.Store<RandomColorState>(reducer, initialState: RandomColorState.defaultState());
- We replace the Flux store with both a state class and a reducer. In Redux, we do not define a class that is responsible for handling state updates. Rather, we create the model to represent the state and a function to describe updates. Then, use them in the instantiation of a Redux
Store
object. - Actions are not held in an overarching wrapper class. Typically in Redux, actions "stand alone" from each other. Our reducer should have cases for all the actions that exist, but unlike Flux there is no need to knit our store and actions together by actually providing an actions instance to the store. Consequently, actions are broken away, pulled directly into the component, and triggered by passing an action instance into
dispatch
- the centralized dispatcher accessible viaprops
if mixing inConnectPropsMixin
, ormapDispatchToProps
. - In Redux, the entire store is not passed to components via props. OverReact Redux's
connect
function uses context to allow you to access the store's state without passing it through multiple layers of components, and also provides a mechanism to map the relevant parts of state directly to props (and optionally only rerender when those props change).connect
is paired with wrapping the component tree in aReduxProvider
component.
NOTE: This document does not attempt to teach how to use Redux. If any of these steps cause confusion on the implementation details of Redux, see the OverReact Redux Documentation.
-
Refactor the actions. Remove the action container class, and replace action instances with classes.
// Before class ExampleActions { final Action<void> randomizeBackgroundColor = Action(); final Action<String> setBackgroundColor = Action(); } // After class RandomizeBackgroundColorAction {} class SetBackgroundColorAction { final String backgroundColor; SetBackgroundColorAction(this.backgroundColor); }
If your
Action
receives a custom class as typing for the action payload, that class could be a great starting point for the creation of your new Redux action.// Before class ExampleActions { final Action<SetBackgroundColorPayload> randomizeBackgroundColor = Action(); } class SetBackgroundColorPayload { String backgroundColor; SetBackgroundColorAction(this.backgroundColor); } // After class SetBackgroundColorAction { final String backgroundColor; SetBackgroundColorAction(this.backgroundColor); }
-
Pull state mutation logic out of the store and into a reducer. Typically within a Flux store you have a
triggerOnActionV2
call that identifies an action and a function used to respond to that action. In many cases, that same logic should be perfect for a reducer.import 'package:w_flux/w_flux.dart' as flux; // Before transition class ExampleFluxStore extends flux.Store { FluxActions _actions; String _backgroundColor = 'Gray'; String get backgroundColor => _backgroundColor; // Constructor ExampleFluxStore(this._actions) { triggerOnActionV2(_actions.changeBackgroundColor, _changeBackgroundColor); } // Function to update the _backgroundColor state field _changeBackgroundColor(_) { _backgroundColor = '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16); } } // After being transitioned to a reducer... ExampleState reducer(ExampleState oldState, dynamic /*or an action type*/ action) { // Assumes an action called ChangeBackgroundColor was created in step 1 if (action is ChangeBackgroundColor) { final color = '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16); return ExampleState.update(color: color); } return oldState; }
-
Create the state model. After pulling out state mutation logic, this should be as simple as renaming the class, not inheriting from the Flux store class, and cleaning up anything unrelated to the actual state fields.
-
Instantiate the store class, using the state model and reducer.
-
Add a
ReduxProvider
around the component tree. This will take in the store instantiated in step 4 as the component'sstore
prop.import 'dart:html'; import 'package:react/react_dom.dart' as react_dom; import 'package:over_react/over_react_redux.dart'; import './store.dart'; import './components/example.dart'; main() { react_dom.render( (ReduxProvider()..store = randomColorStore)( Example()(), ), querySelector('#content')); }
-
Refactor componentry to not longer be a
FluxUiComponent
and instead be a connectedUiComponent2
. In terms of moving away from Flux, the simplest case is that props and prop calls need to be updated. Props will need to be added to make room for the ones consumed by the Redux component, and anyprops.actions
calls need to be updated toprops.dispatch
(unless you're usingmapDispatchToProps
).
In the case a Flux app has a complex store architecture that involves multiple stores, whether they be nested under one store or be completely separate, the transition process has a few additional catches.
This section builds on the simple example, so ensure that section makes sense conceptually before looking at multiple stores.
-
The Flux Store Classes:
RandomColorStore
,MidLevelStore
,LowLevelStore
, andAnotherColorStore
.RandomColorStore
,MidLevelStore
, andLowLevelStore
are nested within each other whileAnotherColorStore
is completely separate.Despite there being four stores, all of them have the same job: handle a single random color property. Therefore, they are all nearly identical objects. Naturally in the real world they would likely be drastically different, but for simplicity sake and keeping the focus on the big picture they are all left the same.
-
The Store Objects:
bigStore
andlittleStore
-
The Actions Class,
RandomColorActions
-
The Actions Object,
randomColorActions
-
The UI: The only complex component is a
BigBlock
component that receivesrandomColorActions
,bigStore
, andlittleStore
as props. Updates are triggered by using the action prop. Naturally now state is accessed via:- props.bigStore.state
- props.bigStore.midLevelStore.state
- props.bigStore.midLevelStore.lowLevelStore.state
- props.littleStore.state
Our
BigBlock
component renders threeSmallBlock
components. Each one connects to a different store's state, with the remaining background color being tied toBigBlock
itself.BigBlock
also has four buttons - one connected to each action that changes the background color property of one of the stores (and thereby updating the component background color).A Flux specific part of the
BigBlock
component is that we need to set theredrawOn
to listen to the store at every level.
import 'package:w_flux/w_flux.dart' as flux;
// A nested store
class RandomColorStore extends flux.Store {
RandomColorActions _actions;
// MidLevelStore looks nearly identical to `RandomColorStore`, but has a different
// store object (`LowLevelStore`) nested within it.
MidLevelStore midLevelStore = MidLevelStore(randomColorActions);
/// Public data
String _backgroundColor = 'gray';
String get backgroundColor => _backgroundColor;
RandomColorStore(this._actions) {
triggerOnActionV2(_actions.changeMainBackgroundColor, _changeBackgroundColor);
}
_changeBackgroundColor(String _) {
_backgroundColor = '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16);
}
}
// A separate store, making the total store count be equal to 4
class AnotherColorStore extends flux.Store {
RandomColorActions _actions;
/// Public data
String _backgroundColor = 'Blue';
String get backgroundColor => _backgroundColor;
AnotherColorStore(this._actions) {
triggerOnActionV2(_actions.changeBlockThreeBackgroundColor, _changeBackgroundColor);
}
_changeBackgroundColor(String _) {
_backgroundColor = '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16);
}
}
import 'package:w_flux/w_flux.dart' as flux;
// A shared action class for all the stores, with an action to update each background color
class RandomColorActions {
final flux.Action<String> changeMainBackgroundColor = flux.Action();
final flux.Action<String> changeBlockOneBackgroundColor = flux.Action();
final flux.Action<String> changeBlockTwoBackgroundColor = flux.Action();
final flux.Action<String> changeBlockThreeBackgroundColor = flux.Action();
}
// Instantiate the actions
RandomColorActions randomColorActions = RandomColorActions();
// Instantiate the stores
RandomColorStore bigStore = RandomColorStore(randomColorActions);
AnotherColorStore littleStore = AnotherColorStore(randomColorActions);
The Redux app doesn't really have any surprises, and at a high level is very similar to the simple example:
- The State Class: we have a class (
ReduxState
) that has four properties:- mainBackgroundColor
- blockOneBackgroundColor
- blockTwoBackgroundColor
- blockThreeBackgroundColor
- Actions: we have four actions as their own classes; one class for each state class property.
- The Reducer: as probably expected, our reducer is also similar to the simple app but with a condition for each action. This reducer is especially contrived because the simplicity of the store data makes each case the same with the only difference being the property the update is pointed at. A more complex usage could leverage Combine Reducers.
- The Store Object: same as the simple example.
- The UI: like the simple example, our component tree is wrapped in a
ReduxProvider
. Otherwise the component architecture is the same as the Flux version, minus the Flux-y parts. We have a connectedUiComponent2
calledBigBlock
that maps the store state to props, which passes the props to its threeLittleBlock
s, with four buttons to trigger the background actions. Naturally all the Flux parts were removed, including theredrawOn
override.
// A regular Dart class that represents the application data model
class ReduxState {
// All of the state fields for the application
String mainBackgroundColor;
String blockOneBackgroundColor;
String blockTwoBackgroundColor;
String blockThreeBackgroundColor;
// A constructor for creating the default app state
ReduxState.defaultState()
: this.mainBackgroundColor = 'gray',
this.blockOneBackgroundColor = 'red',
this.blockTwoBackgroundColor = 'orange',
this.blockThreeBackgroundColor = 'blue';
// A convenience constructor for creating new instances of the state class
ReduxState.update(ReduxState oldState,
{mainBackgroundColor, blockOneBackgroundColor, blockTwoBackgroundColor, blockThreeBackgroundColor})
: this.mainBackgroundColor = mainBackgroundColor ?? oldState.mainBackgroundColor,
this.blockOneBackgroundColor = blockOneBackgroundColor ?? oldState.blockOneBackgroundColor,
this.blockTwoBackgroundColor = blockTwoBackgroundColor ?? oldState.blockTwoBackgroundColor,
this.blockThreeBackgroundColor = blockThreeBackgroundColor ?? oldState.blockThreeBackgroundColor;
}
// An action for updating each state property
class UpdateBackgroundColorAction {}
class UpdateBlockOneBackgroundColorAction {}
class UpdateBlockTwoBackgroundColorAction {}
class UpdateBlockThreeBackgroundColorAction {}
/// A simple reducer that has a case for every action.
///
/// Because the example is relatively contrived to keep things simple, the actions and reducer could be simplified.
/// Ultimately because all the actions are so similar, a different approach could be to have a single action that has
/// a field for every color and just passes those colors into the `ReduxState.update` constructor. However, to
/// illustrate the core idea of an action mapping directly to a state field, the reducer is left more verbose.
ReduxState afterTransitionReducer(ReduxState oldState, dynamic action) {
if (action is UpdateBackgroundColorAction) {
return ReduxState.update(oldState, mainBackgroundColor: getRandomColor());
} else if (action is UpdateBlockOneBackgroundColorAction) {
return ReduxState.update(oldState, blockOneBackgroundColor: getRandomColor());
} else if (action is UpdateBlockTwoBackgroundColorAction) {
return ReduxState.update(oldState, blockTwoBackgroundColor: getRandomColor());
} else if (action is UpdateBlockThreeBackgroundColorAction) {
return ReduxState.update(oldState, blockThreeBackgroundColor: getRandomColor());
}
return oldState;
}
/// A utility method used to generate a random color.
String getRandomColor() {
return '#' + (Random().nextDouble() * 16777215).floor().toRadixString(16);
}
import 'package:redux/redux.dart' as redux;
// Instantiate a store using the reducer and state class
redux.Store reduxStore = redux.Store<ReduxState>(afterTransitionReducer, initialState: ReduxState.defaultState());
You should still only have one Redux store. If that doesn't sound scary, you're in good shape and everything is great! Some libraries won't have an issue with that, and in that case it's as simple as following the steps in the next section.
If having one store sounds unfeasible or raises a lot of concerns, there's more to talk about. The authors of Redux have said (1, 2) that more than one store is not needed, and the exceptions are related to performance and not architecture. The concession is that there are supported ways to have multiple stores, but those are to be used as a last resort and should not be part of an initial refactor attempt.
This leads to the difference in the transition process. Not only do the stores have to come together, but componentry needs to reflect that the data is coming from a single source.
These steps build on those in the simple example above, but code specific to this example can be found in the advanced web example. Additionally, these steps are just a set that make sense for general situations and may not make sense for every library. If the path forward is unclear, they can be referred to for guidance but may need adjustment or supplemental steps.
-
Diagram store and component relationships. While perhaps challenging and time consuming, creating a diagram that illustrates generally which components care about which stores could prove invaluable to planning the update process.
-
Break the refactor into groups that includes the stateful layer and the UI layer. Are there small chunks of the system that can be updated without touching the messiest knots in the system? If so, identify them. If this step seems particularly challenging or reveals large roadblocks, consider an Influx architecture
-
For each group, refactor all the stores. By doing the stores and reducers at the same time, you can completely invalidate a single store at a time. In other words, you can move all the necessary state logic into the new state class and move the state mutation logic into the reducer. Then, the entire Flux store can be deleted.
If you have already created a state class and a reducer for an earlier store group, continue adding to the same reducer and state class. Worth noting is that these app examples use a very simplistic reducer approach, and understanding the options with reducers may be worthwhile. Because there are multiple ways to implement reducers, it is likely one will make more sense than others.
-
Create any new action classes that are necessary. After refactoring a Flux store, it should be clear which actions need to be converted to Redux actions.
-
Refactor components into connected components. Update the component to
UiComponent2
, removing all Flux boilerplate, and wrapping the already declared factory with theconnect
call. -
Move on to the next group. If the refactor has multiple groups of stores and components, move on to the next one.
-
Add a
ReduxProvider
around the component tree. Once it makes sense, wrap the tree in theReduxProvider
component.
The advanced conversion might sound like a huge initiative with numerous unknowns and complexities. If that's the case the first question is whether Redux will offer the benefits to justify a large initiative. Our OverReact Redux documentation includes some of the benefits, and there are many articles online that provide more examples.
If Redux makes sense but the conversion project seems extremely challenging, we have a middle ground: introducing Influx.
Influx is just the term we're giving to an architecture that is both Redux and Flux at the same time. To aid in the transition from Flux to Redux, we have built some utilities to allow Flux and Connected (Redux) components to all talk to the same store. It's a transitional architecture that allows the library to be noncommittal to the state management system, ultimately allowing the transition from Flux to Redux to be a much more incremental process.
tl;dr
If you assess your architecture and are confident you can go straight from Flux to Redux, you should not use Influx. If you are not confident, you should consider it.
The Influx architecture is not a required part of the process, and may make your life more difficult by adding extra steps. There are both advantages and disadvantages, and assessing how large the refactor is will likely provide the largest indication of whether or not it makes sense.
- You can split the effort into very tangible subtasks. Stores can be updated to Redux without necessarily updating the Flux components. Components reliant on multiple stores can talk to Flux stores and Redux stores at the same time. Ultimately, this means a library can pick and choose the areas that can be updated and do them one at a time.
- Inability to update entirely to
Component2
is not a blocker. Sinceconnected
components need to beComponent2
, the Redux refactor may be blocked by those efforts. This option allows the library to update the state system to a Redux friendly architecture while updating components toUiComponent2
andconnect
at the same time. - The workflow is much less complex. While heavily piggybacked off the first two advantages, it's worth noting the workflow benefits of the incremental update. There shouldn't be as many massive merges or code reviews, and the granularity of tasks should make it clearer where manual testing is needed and when tests need to be written or updated. Ultimately this reduces the project complexity and the risk of regressions.
- The actual update process can be easier to reason about in complex scenarios. Influx merges Flux and Redux to provide a "halfway" point that is a fairly straightforward transition both from Flux and to Redux. For those complex scenarios, it may seem daunting to transition straight to Redux, and Influx can lower the barrier.
- Takes extra time as there are ultimately two refactors instead of one (however minimal the second one will be). While ideally the second refactor (from Influx to Redux) should be an easy lift, the middle steps still add effort that could otherwise be avoided.
- Does not provide any performance gains until the transition is complete. The Redux
connected
components will update on every store update as long as Flux is in the mix. This mimics the behavior of Flux, but takes away the largest benefit of connecting to Redux. - Can add complexity and code to a possibly already complex state architecture. There are additional boilerplate and utilities necessary to maintain an Influx architecture that are completely unnecessary to Redux, making Influx more verbose and a little more confusing.
In summary, Influx adds steps to the transition process, but can make the work easier to break up.
If this architecture is appealing, there are a few new classes and utilities it will be beneficial to be aware of.
- Adapted Influx Store: The instance returned from wrapping an Influx store with a
FluxToReduxAdapterStore
. ConnectFluxAdapterStore
: A class that can be used to enableconnectFlux
usage on a component without adding any Redux boilerplate. See ConnectFluxAdapterStore for more information.composeHocs
: If a component takes in multiple stores, it needs to be connected to all of them. This function allows you to combine multipleconnect
orconnectFlux
calls using a flat list, as opposed to nesting them inside each other.connectFlux
: Likeconnect
, but for Flux stores instead of Redux stores. This is useful because it is one step closer to a Redux connected component without being Redux. If, for any reason, implementing the Redux side of Influx is presenting challenges,connectFlux
provides a good middle ground.FluxToReduxAdapterStore
: This is a class that wraps an Influx store and makes it look like a Redux store. It is the cornerstone of Influx because pure Flux components will stay connected to the original store instance, but Redux components and connected Flux components (usingconnectFlux
) will connect to the instantiatedFluxToReduxAdapterStore
object. This works by passing in an Influx store instance and a FluxActions
instance.- Influx store: A Flux store that has implemented the
InfluxStoreMixin
and converted its internal logic (constructor, getters, overrides) to match the Influx pattern. InfluxStoreMixin
: A mixin that attaches to a Flux store in order to add necessary Influx utilities. Namely, the methodinfluxReducer
must be accessible on the FluxStore
class, andinfluxReducer
expects the class to have areduxReducer
method and astate
field. Thestate
field is essentially a proxy for a Redux state class, and makes it so that state class can be built slowly over time without any need to refactor it again.ReduxMultiProvider
: This Component has a similar purpose as that ofcomposeHocs
, but for aReduxProvider
instead. This allows you to "provide" multiple stores via different contexts via one component invocation, as opposed to nesting multipleReduxProvider
s inside each other.
Influx has two steps to the refactor. The first is to go from Flux to Influx. This part of the process can take as long as it needs to and is meant to be an incremental transition. The next step is to go from Influx to Redux, which should be a swift refactor of stores with a light UI refactor.
Goal: Make a game plan for the refactor process. This phase can take as long as it needs to, but should be done in its entirety before attempting Phase 2.
- Diagram store and component relationships. Similar to the Advanced conversion, it still makes sense to understand the current state of the architecture. For Influx in particular, the focus should be on revealing what stores the specific components are reliant on. In some cases, stores may also be reliant on other stores, which should also be noted. At the end of this step, the goal is to be able to point to any component and easily understand all the stores that component relies on.
- Break the refactor into groups that includes the stateful layer and the UI layer. This will also be like Advanced conversion. The difference is that coming out of this step, stores and the UI should be broken into tangible groups to be refactored. These groups will be the core of the refactor plan. Ideally, each group should be independent enough that it can be updated without touching any other group. The groups should also be as small as possible, while also making sense, to allow for the most incremental update possible. If diagramming the library architecture (during the previous step) went well, this should be as easy as looking at the diagram and noting the groups that emerge.
Goal: Update all stores to be either Influx or Redux and update all components to Redux connected components. This phase can be done incrementally, and should be repeated for every store or every group of tightly coupled stores. Consequently, the steps should be read through the lens of that specific store or group of stores.
-
Refactor relevant stores to be flat. State should be lifted up or broken out into their own instances so that stores are not nested. Redux holds firmly that an application should have only one store. However, while refactoring to a single store Redux architecture, Influx can have multiple stores. The rule is that stores cannot be nested.
-
Decide which stores will be Redux and which stores will be Influx. An Influx implementation can have both Redux stores and Influx stores. The rules are:
- A Redux component can talk to both a Redux store and an adapted Influx store at the same time.
- A Flux component can talk to an Influx store and a Flux store at the same time.
- A
connectFlux
component can talk to an adapted Influx store.
When deciding if a store should be converted to Redux or to Influx, the main questions are:
- Can all of the related UI be converted to Redux?
- Can all of the related stores (those that are dependent upon each other), be combined into a single store?
If the answer to both of those is yes, go straight to Redux. If not, go to Influx. A more detailed decision tree can be seen below:
-
Refactor the store. If the store is moving to Redux, follow the simple store conversion. If the store is moving to Influx:
-
Create the Redux actions. Any actions that the connected UI components will need access to should be made into a Redux action.
-
Create a Redux state class for the store. Any state that is mutated by the Redux actions should be present in the Redux state class.
-
Create the Redux reducer for the store. This should just combine the actions and the Redux state class - any action should have a condition within the reducer, and ultimately all fields on the state store should have a way to be updated.
-
Convert the Flux store to an Influx store:
- Add the
InfluxStoreMixin
to the original Flux store. This is what makes it an "Influx" store, and adds the fields required by an Influx architecture. The model forstate
field is the Redux state class created earlier, and it should be passed into the mixin's typing. - Initialize the default Redux state in the Influx constructor.
- Refactor actions passed into
triggerOnActionV2
. Like a Flux store, the Influx store will watch for Flux actions to be triggered. Instead of triggering a function that mutates the inner state however, a callback should pass a corresponding Redux action instance intothis.influxReducer
. - Override the
reduxReducer
getter on the class. ThereduxReducer
getter should point to the Redux reducer function created earlier. - Update the store fields. The end result of this step is that the Influx store (previously the Flux store) should have a getter for every Redux state class field. The getters should point to corresponding values on the
state
field.
Updating getters isn't necessary as the correct fields should be accessible via
influxStoreInstance.state.reduxProperty
, but refactoring the getters means that any Flux components pointing to this store do not need to be refactored. If it is preferable just to remove getters in favor of accessing thestate
fields directly, any original getters or fields that correspond to a value moved to the Redux state class should be removed.import 'package:over_react/over_react_redux.dart'; import 'package:w_flux/w_flux.dart' as flux; // An Influx Store // Add the `InfluxStoreMixin` and use the Redux state class for the generic typing. class ExampleInfluxStore extends flux.Store with InfluxStoreMixin<ReduxStateClass> { ExampleFluxActions _actions; // Point the `reduxReducer` getter at the Redux state's reducer. @override get reduxReducer => lowLevelReduxReducer; /// Use getters to point to state properties. String get backgroundColor => state.backgroundColor; ExampleInfluxStore(this._actions) { state = ReduxStateClass.defaultState(); triggerOnActionV2(_actions.exampleFluxAction, (_) => this.influxReducer(ExampleReduxAction())); } } class ExampleReduxAction {} class ExampleFluxActions { final flux.Action<String> exampleFluxAction = flux.Action(); }
- Add the
-
-
Add adapter stores where necessary. Each store that will be Influx should be wrapped with a
FluxToReduxAdapterStore
to give Flux and Connected Flux access as well. In other words, if the store was updated to Redux directly or will only talk to Redux components, this step can be skipped.FluxToReduxAdapterStore
takes in an Influx store and an actions instance. It should also receive the Influx store class and the Redux state class as generics.final influxAdapterStore = FluxToReduxAdapterStore<ExampleInfluxStore, ReduxStateClass>(exampleInfluxStoreInstance, exampleFluxActionsInstance);
-
Create
Context
instances for the necessary stores. If a component will use multiple stores, aContext
instance is required. The OverReact Redux docs have examples of having multiple stores, which describes the fundamentals of why this is necessary. In the end, it is most likely that the majority ofFluxToReduxAdapterStore
instances will need aContext
instance. -
Wrap the component tree in a either a
ReduxProvider
or aReduxMultiProvider
. If there are multiple stores, aReduxMultiProvider
is more elegant and is encouraged. The store instances passed in should be theFluxToReduxAdapterStore
instances, not the normal Flux store instances.import 'dart:html'; import 'package:over_react/over_react_flux.dart'; import 'package:react/react_dom.dart' as react_dom; import './store.dart'; main() { react_dom.render( (ReduxMultiProvider()..storesByContext = { firstStoreContext: firstStoreAdapter, secondStoreContext: secondStoreAdapter, thirdStoreContext: thirdStoreAdapter, })( // Flux, connectFlux, or Redux connected components can now be used here ), querySelector('#content')); }
-
Refactor components. See the specific component type below for a reminder on which store instance is correct and any "gotchas" in the refactor. In general, remember that if a component talks to multiple stores,
composeHocs
can be used to simplify the connected factory declarations.For code examples on what this could look like, compare the different components in the advanced web example.
- A Flux component will operate exactly the same way.
- Pass in the Influx store instance as a prop (which should already be done). Not the
FluxToReduxAdapterStore
instance, but the same Influx store instance used to instantiate theFluxToReduxAdapterStore
object. - The actions prop should also be the same action class instance passed into the
FluxToReduxAdapterStore
constructor.
- Pass in the Influx store instance as a prop (which should already be done). Not the
- A Redux component will use the
connect
function.- The
pure
parameter onconnect
should be set tofalse
, or else it will not receive regular updates. - If the component tree was wrapped in a
ReduxMultiProvider
, thecontext
parameter should also be set to the relevant store context.
- The
- A Connected Flux component is essentially the same as a Redux component.
- The only exception is that
mapDispatchToProps
(Reduxconnect
parameter) ismapActionsToProps
for a Connected Flux component.
- The only exception is that
- A Flux component will operate exactly the same way.
-
Continue this process of refactoring stores and components until most components are Redux components.
Goal: Remove any Influx stores and combine the Redux stores.
-
Remove the Influx stores. Because each store should be backed completely by a Redux state class, one should be able to completely delete the Influx store.
-
Remove the Flux actions class.
-
Combine the Redux Stores. Redux holds strongly that an application should have a single store as the source of truth. A store can be a complex class with properties that are essentially their own state model, but they should live within a single class. If this is not possible, multiple stores can be used but it is highly discouraged.
// Before combining class FirstStateClass { var field1; var field2; } class SecondStateClass { var field3; var field4; } // Possible refactor options class WrapperStateClass { FirstStateClass firstState; SecondStateClass secondState; } class FlattenedStateClass { var field1; var field2; var field3; var field4; }
-
Instantiate the Redux stores.
FluxToReduxAdapterStore
instances can be removed during this step and just switched out with Redux store instantiations.redux.Store reduxStore = redux.Store<ReduxState>(reduxReducer, initialState: ReduxState.defaultState());
-
Update context instances. If, after combining the stores, there is only a single store instance, all context instances can be removed. This is the best practice and it is rare that context will need to be used. If there are multiple stores, a context instance will be necessary for each store.
-
Refactor UI. Generally static analysis should indicate the majority of things that need to be fixed after refactoring the store architecture, but the following are the cases that the analyzer will be catching:
- (Ideally) Remove any
combineHocs
andReduxMultiProvider
calls. These should be switched out for simpleReduxProvider
andconnect
calls. If you are using multiple stores however, they may still be useful in some cases, but it is likely there will still be some cleanup involved. - Refactor any
state
references. How extensive this is depends on the new store architecture and how closely it matches the Influx architecture, but it is extremely likely that fields got moved and now components are looking for state in the wrong place. - Update any
connectFlux
toconnect
. Up until Phase 3,connectFlux
components would have functioned without an issue, but they now need to be moved over entirely toconnect
.
- (Ideally) Remove any
Woohoo! Your library should now be updated to Redux!!
If you would like to start the Influx refactor process but feel it is best to wait to build out the Redux side of Influx, the ConnectFluxAdapterStore
be used to enable connectFlux
usage. There is a verbose way to do this without this adapter (in other words, using FluxToReduxAdapaterStore
), but to minimize boilerplate the ConnectFluxAdapterStore
was created.
Note: connectFlux
was always meant to be a stepping stone towards Redux. The utilities it is built upon expects there to be a Redux implementation, and thus is not an optimized solution. The goal of ConnectFluxAdapterStore
is to add an additional possible incrementation point and should not be treated as a final design pattern.
To start, a simple Flux store may look something like:
import 'package:w_flux/w_flux.dart' as flux;
import 'package:over_react/over_react_flux.dart';
class FluxActions {
final flux.Action<int> updateExample = flux.Action();
}
class ExampleStore extends flux.Store {
FluxActions _actions;
var _example = 0;
int get example => _example;
TestConnectableFluxStore(this._actions) {
triggerOnActionV2(_actions.updateExample, _updateExample);
}
void _incrementAction(int newNumber) {
_example = newNumber;
}
}
final actions = FluxActions();
final store = ExampleStore(actions);
Without the ConnectFluxAdapterStore
, to enable connectFlux
usage without Redux, the store would look like:
import 'package:w_flux/w_flux.dart' as flux;
import 'package:over_react/over_react_flux.dart';
class FluxActions {
final flux.Action<int> updateExample = flux.Action();
}
class ExampleStore extends flux.Store with InfluxStoreMixin<Null> {
FluxActions _actions;
@override
get reduxReducer => noopReducer;
var _example = 0;
int get example => _example;
TestConnectableFluxStore(this._actions) {
triggerOnActionV2(_actions.updateExample, _updateExample);
}
void _incrementAction(int newNumber) {
_example = newNumber;
}
}
// Note the addition of a "reducer" that does nothing.
Null noopReducer(Null oldState, dynamic actions) {
return oldState;
}
final actions = FluxActions();
final store = ExampleStore(actions);
final adapter = store.asReduxStore(actions);
With the ConnectFluxAdapterStore
, your original Flux store would look like:
import 'package:w_flux/w_flux.dart' as flux;
import 'package:over_react/over_react_flux.dart';
class ExampleStore extends flux.Store {
FluxActions _actions;
var _example = 0;
int get example => _example;
TestConnectableFluxStore(this._actions) {
triggerOnActionV2(_actions.updateExample, _updateExample);
triggerOnActionV2(_actions.resetAction, _resetAction);
}
void _incrementAction(int newNumber) {
_example = newNumber;
}
}
final actions = FluxActions();
final store = ExampleStore(actions);
// Note that the only difference is an extra instantiation step
final adapter = store.asConnectFluxStore(actions);
That's all ConnectFluxAdapterStore
is! Here's a breakdown of the the usage rules:
- The usage in the UI layer is the same as
FluxToReduxAdapterStore
. - Redux cannot be used to update the store. Obviously since there is no reducer, Redux cannot talk to the store without workarounds.
- A
connected
component will still receive updates, but that would be an anti-pattern. If special circumstances dictate that this saves a significant amount of effort, then it will work, but if Redux is being utilized then thestate
field should be backed by a Redux state model.
To reduce the boilerplate and abstract some of the details a little more, two extension methods have been added:
-
asReduxStore
: to be used on a Flux store usingInfluxStoreMixin
,asReduxStore
returns aFluxToReduxAdapter
store instance.Example:
import 'package:w_flux/w_flux.dart' as flux; import 'package:over_react/over_react_flux.dart'; class ExampleFluxStore extends flux.Store with InfluxStoreMixin { // ... store implementation } class FluxActionsExample { // ... action declarations } final actions = FluxActionsExample(); final fluxStore = ExampleFluxStore(); // adapter without the extension method final verboseAdapterStore = FluxToReduxAdapterStore(fluxStore, actions); // the same thing with `asReduxStore` final succinctAdapterStore = fluxStore.asReduxStore(actions);
-
asConnectFluxStore
: to be used on a Flux store not usingInfluxStoreMixin
,asConnectFluxStore
returns aConnectFluxAdapaterStore
store instance.Example:
import 'package:w_flux/w_flux.dart' as flux; import 'package:over_react/over_react_flux.dart'; class ExampleFluxStore extends flux.Store { // ... store implementation } class FluxActionsExample { // ... action declarations } final actions = FluxActionsExample(); final fluxStore = ExampleFluxStore(); // adapter without the extension method final verboseAdapterStore = ConnectFluxAdapterStore(fluxStore, actions); // the same thing with `asConnectFluxStore` final succinctAdapterStore = fluxStore.asConnectFluxStore(actions);