A Dart wrapper for React Redux, providing targeted state updates.
- Purpose
- Examples
- Using it in your project
- ReduxProvider
- Hooks
- Connect
- Using Multiple Stores
- Using Redux DevTools
OverReact Redux is a Dart wrapper around React Redux, a UI binding library that allows for better Redux integration with React. This integration allows for APIs that align more closely with React design patterns while also providing more direct access to store data.
For a full list of benefits, see https://react-redux.js.org/introduction/why-use-react-redux.
While there are many benefits of using the library, a primary one is OverReact Redux allows for targeted updates
that only update components which receive data that has changed, rather than the whole component tree (as is the
behavior with React). By utilizing the connect()
function in conjunction with mapStateToProps()
, a component will
only update when a piece of information it uses is updated.
There are some individual component examples within the web/over_react_redux
directory.
Each example illustrates a different variation or use case of OverReact Redux. Additionally, the store files contain
comments that call out specifics pertaining to that example and provides further explanation.
To run and experiment with the demo:
pub get
webdev serve
- Navigate to
localhost:8080/over_react_redux/
- If you have the React DevTools,
you can view the isolated state updates based on the
mapStateToProps
when you turn on "Highlight updates when components render."
There is a "Todo" example app built with OverReact Redux within the app/over_react_redux
directory.
This app illustrates a full-scale implementation of an application that handles all of the data flow using redux.
To run and experiment with the "Todo" app:
cd app/over_react_redux/todo_client
pub get
webdev serve
- Navigate to
localhost:8080
- If you have the React DevTools,
you can view the isolated state updates based on the
mapStateToProps
when you turn on "Highlight updates when components render."
-
Add the
redux
package as a dependency in yourpubspec.yaml
.dependencies: redux: '>=3.0.0'
-
Create your store and reducer. See the example store for ways to do this.
-
Import OverReact Redux and your store into your
index.dart
.import 'package:over_react/over_react_redux.dart';
-
Wrap your component tree in a
ReduxProvider
and pass in the store.import 'package:over_react/over_react_redux.dart'; import 'package:over_react/react_dom.dart' as react_dom; main() { react_dom.render( (ReduxProvider()..store = fooStore)( // ... React component tree with connected components ), mountNode, ); }
-
Import OverReact Redux and your store into the file with your component.
-
Update your component class.
- Add a new
UiFactory
, without the usual annotation, and wrap it withconnect()
. - Add the
ConnectPropsMixin
to your props class if needed. This mixin is needed if either of the following are true:- You do not use the
mapDispatchToProps
parameter onconnect
. - You do use
mapDispatchToProps
but want access to theprops.dipsatch
function. When usingmapDispatchToProps
, by default thedispatch
property onprops
is removed. It can be added back usingConnectPropsToMixin
.
- You do not use the
// AppState is a class that represents the application's state and can be defined in the same file as the store. UiFactory<FooProps> Foo = connect<AppState, FooProps>( ... )(_$Foo); // Use the ConnectPropsMixin to gain access to React Redux's dispatch function, which can be accessed via // props.dispatch. mixin FooPropsMixin on UiProps { ... } class FooProps = UiProps with ConnectPropsMixin, FooPropsMixin; class FooComponent extends UiComponent2<FooProps> { ... }
- Add a new
Redux uses React 16's Context
API to pass information through the component tree. Consequently, the tree must be
wrapped in a Provider
to handle the context.
For this purpose, OverReact Redux has the ReduxProvider
component. It is required to take in a store instance, and can
optionally take in a context object when using multiple stores.
Example:
react_dom.render(
(ReduxProvider()..store = fooStore)(
// ... React component tree with connected components
),
mountNode,
);
A wrapper around the JS react-redux connect
function that supports OverReact components.
![TIP] connect still works and is supported. However, we recommend using the hooks API as the default.
Example:
UiFactory<CounterProps> Counter = connect<CounterState, CounterProps>(
mapStateToProps: (state) => (Counter()
..count = state.count
),
mapDispatchToProps: (dispatch) => (Counter()
..increment = (() => dispatch(IncrementAction()))
),
)(_$Counter);
Note that any required props assigned in connect must have their validation disabled; see docs here for more info.
For example:
mixin CounterPropsMixin on UiProps {
// Set in connect.
late int count;
late void Function() increment;
// Must be set by consumers of the connected compoennt.
late String requiredByConsumer;
}
@Props(disableRequiredPropValidation: {'count', 'increment'})
class CounterProps = UiProps with CounterPropsMixin, OtherPropsMixin;
-
-
Used for selecting the part of the data from the store that the connected component needs.
- Called every time the store state changes.
- Receives the entire store state, and should return an object of data this component needs.
-
If you need access to the props provided to the connected component you can use
mapStateToPropsWithOwnProps
, the second argument will beownProps
.See: https://react-redux.js.org/using-react-redux/connect-mapstate#defining-mapstatetoprops
-
If you need component-instance-specific initialization, such as to set up instance based selectors with memoization, you can use
makeMapStateToProps
ormakeMapStateToPropsWithOwnProps
to define factory functions, they will be called once when the component instantiates, and their returns will be used as the actualmapStateToProps
.See: https://redux.js.org/recipes/computing-derived-data#sharing-selectors-across-multiple-components
See: https://react-redux.js.org/api/connect#factory-functions
-
-
-
Called with dispatch as the first argument.
- You can make use of this by returning new functions that call dispatch() inside themselves, and either pass in a plain action directly or pass in the result of an action creator.
-
If you need access to the props provided to the connected component you can use
mapDispatchToPropsWithOwnProps
, the second argument will beownProps
. -
If you need component-instance-specific initialization, such as to set up instance based selectors with memoization, you can use
makeMapDispatchToProps
ormakeMapDispatchToPropsWithOwnProps
to define factory functions, they will be called once when the component instantiates, and their returns will be used as the actualmapDispatchToProps
.See: https://redux.js.org/recipes/computing-derived-data#sharing-selectors-across-multiple-components
See: https://react-redux.js.org/api/connect#factory-functions
-
-
- Defines how the final props for the wrapped component are determined.
- If you do not provide
mergeProps
, the wrapped component receives the default:{...ownProps, ...stateProps, ...dispatchProps}
-
- Does a simple
==
check by default. - Takes a function with the signature
(next: TReduxState, prev: TReduxState) => bool
. - When pure, compares the incoming store state to its previous value in order to assess if this connected
component should update. In the case that
mapStateToProps
is only concerned with a small piece of state but is also expensive to run, passing in a custom function forareStatesEqual
may be beneficial to performance.
- Does a simple
-
- Does a shallow Map equality check by default.
- Takes a function with the signature
(next: TProps, prev: TProps) => bool
. - When pure, compares incoming props to its previous value. This comparison can be used to conduct operations such as being part of a process to whitelist certain props.
-
- Does a shallow Map equality check by default.
- Takes a function with the signature
(next: TProps, prev: TProps) => bool
. - When pure, compares the results of
mapStateToProps
with its previous value. Similar to other equality check callbacks, this provides the opportunity to optimize performance.
-
- Does a shallow Map equality check by default.
- Takes a function with the signature
(next: TProps, prev: TProps) => bool
. - Can be passed in for small performance improvements. Two possible cases are to implement
deepEqual
orstrictEqual
. > See: https://react-redux.js.org/api/connect#aremergedpropsequal-next-object-prev-object-boolean
-
- Can be utilized to provide a custom context object created with
createContext
. context
can be used to utilize multiple stores.For more information on having multiple stores, see Using Multiple Stores.
- Can be utilized to provide a custom context object created with
-
- If
true
(default), connect performs several equality checks that are used to avoid unnecessary calls tomapStateToProps
,mapDispatchToProps
,mergeProps
, and ultimately torender
. These includeareStatesEqual
,areOwnPropsEqual
,areStatePropsEqual
, andareMergedPropsEqual
. - While the defaults are probably appropriate 99% of the time, you may wish to override them with custom implementations for performance or other reasons.
- If
-
- If
true
, theref
prop provided to the connected component will forward onto and return the wrapped component.
- If
OverReact exposes wrappers around React Redux hook APIs, which serve as an alternative to the existing connect()
Higher Order Component.
These APIs allow you to subscribe to the Redux store and dispatch actions, without having to wrap your components
in connect()
.
Tip
We recommend using the React-Redux hooks API as the default approach in your React components.
The existing connect API still works and will continue to be supported, but the hooks API is simpler, requires less OverReact boilerplate, and doesn't involve suppressing validation for required props (see connect docs for more info).
As with connect()
, you should start by wrapping your entire application in a
ReduxProvider
component to make the store available throughout the component tree:
import 'package:over_react/over_react_redux.dart';
import 'package:over_react/react_dom.dart' as react_dom;
main() {
react_dom.render(
(ReduxProvider()..store = yourReduxStoreInstance)(
// Function components that use the hooks in this section go here.
),
querySelector('#id_of_mount_node'));
}
From there, you may import any of the hook APIs listed below and use them within your function components.
TValue useSelector<TReduxState, TValue>(
TValue Function(TReduxState state) selector, [
bool Function(TValue tNextValue, TValue tPrevValue) equalityFn,
]);
Allows you to extract data from the Redux Store
state, using a selector
function.
The use of this hook will also subscribe your component to the Redux Store
, and run your selector
whenever
an action is dispatched.
- The
selector
should be pure since it is potentially executed multiple times and at arbitrary points in time. - The
selector
is approximately equivalent to themapStateToProps
argument ofconnect
conceptually. - The
selector
will be called with the entire ReduxStore
state as its only argument. - The
selector
will be run whenever the function component renders.
By default, the return value of selector
is compared using strict JavaScript (===
) equality. If you want to
customize how equality is defined, pass a comparator function to the equalityFn
argument.
If you need to use a selector with custom Context
, use createSelectorHook
instead.
See the react-redux JS documentation for more details.
This example assumes that your
Counter
component is rendered as the descendant of aReduxProvider
component that is wired up to a ReduxStore
instance with aCounterState
instance containing a field calledcount
.
import 'package:over_react/over_react.dart';
import 'package:over_react/over_react_redux.dart';
import 'counter_state.dart';
mixin CounterProps on UiProps {}
UiFactory<CounterProps> Counter = uiFunction(
(props) {
final count = useSelector<CounterState, int>((state) => state.count);
return Dom.div()('The current count is $count');
},
$CounterConfig, // ignore: undefined_identifier
);
If you need to use multiple selectors in a single component, use createSelectorHook to
shadow useSelector
as shown below to remove a bunch of unnecessary boilerplate as shown in the example below.
Consider the previous example, but instead of only needing to access count
from the store, you need to access count
,
and two other field values as well. Using useSelector
for all of these can get a little messy:
import 'package:over_react/over_react.dart';
import 'package:over_react/over_react_redux.dart';
import 'counter_state.dart';
mixin CounterProps on UiProps {}
UiFactory<CounterProps> Counter = uiFunction(
(props) {
final count = useSelector<CounterState, int>((state) => state.count);
final foo = useSelector<CounterState, String>((state) => state.foo);
final bar = useSelector<CounterState, Map>((state) => state.bar);
return Dom.div()('The current $foo count is $count. $bar my dude.');
},
$CounterConfig, // ignore: undefined_identifier
);
Instead of needing to declare those generic parameters each time on useSelector
, shadow it like so:
import 'package:over_react/over_react.dart';
import 'package:over_react/over_react_redux.dart';
import 'counter_state.dart';
/// All the types for state fields within `CounterState` will be inferred!
final useSelector = createSelectorHook<CounterState>();
mixin CounterProps on UiProps {}
UiFactory<CounterProps> Counter = uiFunction(
(props) {
final count = useSelector((state) => state.count);
final foo = useSelector((state) => state.foo);
final bar = useSelector((state) => state.bar);
return Dom.div()('The current $foo count is $count. $bar my dude.');
},
$CounterConfig, // ignore: undefined_identifier
);
CAUTION: Be sure to not export the shadowed value of
useSelector
unless you know exactly what you're doing, and the consumers of your library expect the hook to always have the context of the state you parameterize it with.
dynamic Function(dynamic action) useDispatch();
Returns a reference to the Redux Store.dispatch()
function.
You may use it to dispatch actions as needed.
If you need to dispatch actions within a custom Context
, use createDispatchHook
instead.
See the react-redux JS documentation for more details.
This example assumes that your
Counter
component is rendered as the descendant of aReduxProvider
component that is wired up to a ReduxStore
instance with aCounterState
instance containing a field calledcount
.It also assumes that you have two actions wired up to your reducer -
IncrementAction
andDecrementAction
.
import 'package:over_react/over_react.dart';
import 'package:over_react/over_react_redux.dart';
import 'counter_state.dart';
mixin CounterProps on UiProps {}
UiFactory<CounterProps> Counter = uiFunction(
(props) {
final count = useSelector<CounterState, int>((state) => state.count);
final dispatch = useDispatch();
return Dom.div()(
Dom.div()('The current count is $count'),
(Dom.button()
..onClick = (_) {
dispatch(IncrementAction());
}
)('+'),
(Dom.button()
..onClick = (_) {
dispatch(DecrementAction());
}
)('-'),
);
},
$CounterConfig, // ignore: undefined_identifier
);
Store<TReduxState> useStore<TReduxState>();
Returns a reference to the same Redux Store
that was passed in to the ReduxProvider
component that the function component using the hook is a descendant of.
This hook should probably not be used frequently. Prefer useSelector()
as your primary choice.
However, this may be useful for less common scenarios that do require access to the Store
,
such as replacing reducers.
If you need access to a specific store from a nested ReduxProvider
with a custom Context
,
use createStoreHook
instead.
See the react-redux JS documentation for more details.
The ReduxProvider
component allows you to specify an alternate Context
via props.context
.
This is useful if you're building a complex reusable component, and you don't want your store to collide with any
Redux store your consumers' applications might use, or you're using multiple Redux stores
in your application.
To access an alternate context via the hooks API, use the hook creator functions:
import 'package:over_react/over_react.dart';
import 'package:over_react/over_react_redux.dart';
import 'package:redux/redux.dart';
// ------------------------------------
// 1. Declare the custom context
// ------------------------------------
final MyContext = createContext();
final useSelector = createSelectorHook<MyState>(MyContext);
final useDispatch = createDispatchHook(MyContext);
// This should probably not be used frequently. Prefer `createSelectorHook` as your primary choice.
// However, this may be useful for less common scenarios that do require access to the `Store`,
// such as replacing reducers.
final useStore = createStoreHook<MyState>(MyContext);
final myStore = Store(myReducer);
// ------------------------------------
// 2. Create a function component that
// uses the shadowed `useSelector`.
// ------------------------------------
mixin MyComponentProps on UiProps {}
UiFactory<MyComponentProps> MyComponent = uiFunction(
(props) {
final count = useSelector((state) => state.count);
// This should probably not be used frequently. Prefer `createSelectorHook` as your primary choice.
// However, this may be useful for less common scenarios that do require access to the `Store`,
// such as replacing reducers.
final store = useStore();
final dispatch = useDispatch();
return Dom.div()('The current count is $count');
},
$MyComponentConfig, // ignore: undefined_identifier
);
// ------------------------------------
// 3. Render the function component
// nested within the ReduxProvider
// that is wired up to the
// custom context / store.
// ------------------------------------
main() {
final app = (ReduxProvider()
..context = MyContext
..store = myStore
)(
MyComponent()(),
);
react_dom.render(app, querySelector('#id_of_mount_node'));
}
CAUTION: Be sure to not export the shadowed values of
useSelector
,useDispatch
oruseStore
unless you know exactly what you're doing, and the consumers of your library expect the hook to always have the context of the state you parameterize it with.
See the react-redux JS documentation for more details.
An application can have multiple stores by both utilizing the context
prop of ReduxProvider
and setting the
context
parameter on connect
. While this is possible, it is not recommended.
See: https://stackoverflow.com/questions/33619775/redux-multiple-stores-why-not
Multiple Stores Example:
Store store1 = new Store<CounterState>(counterStateReducer, initialState: new CounterState(count: 0));
Store store2 = new Store<BigCounterState>(bigCounterStateReducer, initialState: new BigCounterState(bigCount: 100));
UiFactory<CounterProps> Counter = connect<CounterState, CounterProps>(
mapStateToProps: (state) => (Counter()..count = state.count)
)(castUiFactory(_$Counter));
UiFactory<CounterProps> BigCounter = connect<BigCounterState, CounterProps>(
mapStateToProps: (state) => (BigCounter()..count = state.bigCount),
context: bigCounterContext,
)(castUiFactory(_$Counter));
react_dom.render(
Dom.div()(
(ReduxProvider()..store = store1)(
(ReduxProvider()
..store = store2
..context = bigCounterContext
)(
Dom.div()(
Dom.h3()('BigCounter Store2'),
BigCounter()(
Dom.h4()('Counter Store1'),
Counter()(),
),
),
),
),
), querySelector('#content')
);
In the case that you need to have multiple stores, here are the steps to do so:
- Create a
Context
instance to provide to theReduxProvider
and the components that will be in that context.final fooContext = createContext();
- In the
connect
function wrapping the component, pass in the context instance.UiFactory<BarProps> Bar = connect<BarState, BarProps>( // ... mapStateToProps context: fooContext, )(castUiFactory(_$Bar));
- Add an additional
ReduxProvider
, with itscontext
prop set to the next Context instance and thestore
prop set to your additional store.// ... Wrapped in a reactDom.render() (ReduxProvider()..store = store1)( (ReduxProvider() ..store = store2 ..context = bigCounterContext )( // ... connected componentry ), )
To use multiple stores with function components / hooks, check out the Custom Context for hooks example.
Redux DevTools can be set up easily by adding only a few lines of code.
Additional information about
redux_dev_tools
andDevToolsStore
s can be found here
- Add
redux_dev_tools
as a dev dependency in yourpubspec.yaml
.dev_dependencies: redux_dev_tools: ^0.4.0
- Import
redux_dev_tools
into your store file.import 'package:redux_dev_tools/redux_dev_tools.dart';
- Change your
Store
to aDevToolsStore
instance and add the constantoverReactReduxDevToolsMiddleware
to your middleware.- var store = new Store<AppState>( + var store = new DevToolsStore<AppState>( /*ReducerName*/, initialState: /*Default App State Object*/, + middleware: [overReactReduxDevToolsMiddleware], );
NOTE: You should revert back to a normal
Store
without theoverReactReduxDevToolsMiddleware
prior to making your code public (via publishing a package or deploying to production) as it will be less performant and could be a security risk. - Get the Redux Devtools extension:
You can run your code and open the devtools in your browser!
overReactReduxDevToolsMiddlewareFactory
is also available to pass options into the initialization of the redux dev tools.
var store = new DevToolsStore<AppState>(
/*ReducerName*/,
initialState: /*Default App State Object*/,
middleware: [overReactReduxDevToolsMiddlewareFactory(name: 'Some Custom Instance Name')],
)
In order to display the properties of Dart based Action
s and State
in the DevTools they must implement a toJson
method.
toJson
can be manually added to the classes, or added with the help of something like the json_serializable or built_value and its serializers. In the event that a value is not directly encodeable to json
, we will make an attempt to call toJson
on the value.
State Example:
class FooState {
bool foo = true;
Map<String, dynamic> toJson() => {'foo':foo};
}
When converted, the result of toJson
will be used to present the entire state.
{
"foo": true
}
Action Example:
class FooAction {
bool foo = false;
Map<String, dynamic> toJson() => {'foo':foo};
}
When converted, the class name will be the type
property and toJson
will become the payload
{
"type": "FooAction",
"payload": {
"foo": false
}
}
Action (Enum) Example:
enum FooAction {
ACTION_1,
ACTION_2,
}
When an enum Action
is used the value of the action in the enum will be used
{"type": "ACTION_1"}
For a more encoding details check out the redux_remote_devtools