Managing asynchronous state is a complex problem for front-end apps. With a push-oriented platform like Diffusion, ensuring that updates are properly dispatched across your entire application is vital for building reliable & consistent app experiences.
Within the React ecosystem, one of the most popular state management libraries is Redux, which provides dependable ordering and visibility of state transitions throughout an entire application. However, Redux only provides synchronous actions (i.e. operations that return changes to data immediately). The other half of the equation is Redux Thunk, which operates within Redux and lets you easily perform asynchronous actions on the state tree.
This guide will help you setup Diffusion with Redux + Redux Thunk, and demonstrate some basic best practices on managing Diffusion state within your application with a live updating Counter. While this guide is based on React Native, the examples provided are equally applicable for using Diffusion and Redux with base React.
If you're unfamiliar with Redux, make sure to read the Getting Started with Redux walkthrough. If you're not sure what the difference between normal and asynchronous state transitions are, or why you need to use Redux Thunk, the Redux Advanced Tutorial provides more insight into the details of asynchronous actions.
Set up
To begin, you'll need a project setup with Diffusion and React. If you don't have an existing project, we recommend you follow the Using Diffusion with React Native guide first, as it provides an easy walkthrough to get Diffusion and React Native working together.
Once you have your initial project ready, we'll add Redux, React Redux, and Redux thunk to the dependency list:
npm install -s redux react-redux redux-thunk
Now that you have Redux and Redux Thunk available, you need to create a store that will contain all of the application state, as well as the actions you wish to make available. create the following files in your project directory: types.js, actions.js
, reducer.js
, and store.js
.
In actions.js, only import the types for now:
import * as Types from './types';
In reducer.js
, add the following code:
import * as Types from './types';
const initialState = {};
export const reducer = (state = initialState, action) => {
return state;
}
And in store.js
, add the following:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { reducer } from './reducer';
export const store = createStore(reducer, applyMiddleware(thunk));
You now have the bare minimum of a Redux store implemented! The final setup step is to wire the store into the app. If you're following on from the Diffusion with React Native tutorial, simply replace `App.js` with the following:
import 'node-libs-react-native/globals';If you're not using React Native, or are working against an existing project, ensure that you're wrapping your main App component with react-redux's Provider component in your application's entry file.
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Provider } from 'react-redux';
import { store } from './store';
export function App () {
return (
<Viewstyle={styles.container}>
</View>
);
}
export default function Container() {
return (
<Provider store={store}>
<App/>
</Provider>
);
}
const styles = StyleSheet.create({
container: {
flex:1,
backgroundColor:'#fff',
alignItems:'center',
justifyContent:'center',
},
});
You now have the basic setup for a React project using Redux + Thunk! The next step will walk you through handling Diffusion's connection state management with Redux.
Connection management
When you call Diffusion#connect, the client SDK attempts to connect to the remote Diffusion service, and if authentication succeeds you'll be provided a Session after a brief period of time. This Session can be in one of several connection states; connected, disconnected, reconnecting or closed. Which state a Session is in can change at any time - especially on mobile, where apps can be put into the background or closed unpredictably.
If you have app components that rely on Diffusion, managing the initiation and status of connections is vital for a bug-free application. This is the power of Redux; it provides a centralised way of managing updates, ensuring every component has a consistent view on your application state model. Let's start by implementing some Redux actions to handle connecting to Diffusion.
Add the following action types to types.js:
export const SET_SESSION = 'SET_SESSION';
export const SET_CONNECTED = 'SET_CONNECTED';
In action.js, add the following:
import { connect as connectDiffusion, datatypes, topics } from 'diffusion';
// Standard Redux actions - they operate synchronously
export const setConnected = (connected) => {
return {
type: Types.SET_CONNECTED,
connected
}
}
export const setSession = (session) => {
return {
type: Types.SET_SESSION,
session
}
}
// Redux Thunks - they dispatch asynchronous actions
export const connect = (options) => {
return async (dispatch) => {
const session = await connectDiffusion({
...options
});
session.on('close', () => {
dispatch(setConnected(false));
dispatch(setSession(null))
});
dispatch(setSession(session));
dispatch(setConnected(true));
return session;
}
}
export const close = () => {
return async (dispatch, getState) => {
const { session } = getState();
if (session) {
session.close();
}
}
}
This creates four actions. The first two are standard Redux actions - used to update the state store with the connection status and a reference to the active Diffusion session. The second two actions are Redux Thunks - actions that allow behaviour to be tied to asynchronous state.
In the connect action, it's important to attach the close listener - this is what allows your application to be notified when the session is closed. Any changes to a session's state should be handled here, and then dispatched via Redux actions.
Now that you've setup the basic actions, the next step is to handle them within reducer.js. Change the initialState variable to the following:
const initialState = {
session: null,
connected: false,
}
And set the reducer function to:
export const reducer = (state = initialState, action) => {
switch (action.type) {
case Types.SET_CONNECTED:
return {
...state,
connected: action.connected
};
case Types.SET_SESSION:
return {
...state,
session: action.session
};
default:
return state;
}
}
The final step is to make use of these new actions within your application. First, within App.js (or wherever you've defined your main app component), import the actions as well as the useDispatch and useSelector methods from react-redux:
import { Text, Button } from 'react-native';
import { connect, close } from './actions'; import { Provider, useDispatch, useSelector } from 'react-redux';
Finally, replace your App component with the following:
export function App () {
const connected = useSelector(state => state.connected);
const dispatch = useDispatch();
const details = {
host: 'Your Diffusion hostname',
principal: 'Your username',
credentials: 'Your password',
reconnect: false
}
const toggleConnect = () => {
if (connected) {
dispatch(close());
} else {
dispatch(connect(details));
}
}
return (
<View style={styles.container}>
<Button onPress={toggleConnect} title={connected ? 'Disconnect' : 'Connect'}/>
<Text>Diffusion status: {connected ? 'connected' : 'not connected'}</Text>
</View>
);
}
The useSelector call allows the component to be updated whenever the store is changed (by the reducer) - you can choose exactly what parts of the state model that the component depends on. The useDispatch method is what's used to call the Redux Thunk actions; the Redux Thunk middleware will automatically detect that you're dispatching an asynchronous action, and handle the coordination of events within Redux.
If you now set the credentials to your Diffusion settings and run this app, you will be able to see how connecting and closing a Session through Redux actions results in your application state being updated automatically.
Subscribing to topics
Now that you have Diffusion connected, the next step is to interact with live data. Much like Redux provides a local application state model, Diffusion provides a distributed state model that can be updated across the Internet in real-time. Handling asynchronous updates from Diffusion is done via Topics as part of the Pub/Sub functionality - which maps very naturally to Redux Actions. For this tutorial, we'll create an interactive counter. Any number of applications can connect and update the counter, and updates will be sent instantly to every user of the app.
Let's start by adding a few new action types to types.js:
export const COUNTER_SUBSCRIBED = 'COUNTER_SUBSCRIBED';
export const COUNTER_UPDATE = 'COUNTER_UPDATE';
Next, in reducer.js, add the counter state to your initialState variable:
const initialState = {
session: null,
connected: false,
counter: {
value: 0,
loaded: false
}
}
And add the following action handlers to the reducer function:
case Types.COUNTER_SUBSCRIBED:
return {
...state,
counter: {
...state.counter,
loaded: true
}
}
case Types.COUNTER_UPDATE:
return {
...state,
counter: {
...state.counter,
...action.counter
}
}
export const counterSubscribed = () => {
return {
type: Types.COUNTER_SUBSCRIBED
}
}
export const counterUpdate = (counter) => {
return {
type: Types.COUNTER_UPDATE,
counter
}
}
export const subscribeToCounter = () => {
return async (dispatch, getState) => {
const { connected, session } = getState();
if (session && connected) {
return session.select("counter").then(() => {
dispatch(counterSubscribed());
});
}
throw new Error('Unable to subscribe when not connected');
}
}
export const connect = (options) => {
return async (dispatch) => {
const session = await connectDiffusion({
...options
});
// Establish a single value stream to pass topic updates to the reducer
session.addStream('counter', datatypes.json()).on('value', (path, spec, value) => {
dispatch(counterUpdate(value.get()));
});
session.on('close', () => {
dispatch(setConnected(false));
dispatch(setSession(null));
});
dispatch(setSession(session));
dispatch(setConnected(true));
return session;
}
}
import { useEffect } from 'react'; import { subscribeToCounter } from './store/diffusion/actions';
And update the main component body to make use of the new Redux state and action:
export function App () {
const connected = useSelector(state => state.connected);
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
useEffect(() => {
if (connected) {
dispatch(subscribeToCounter());
}
}, [connected]);
const details = {
host: 'Your Diffusion hostname',
principal: 'Your username',
credentials: 'Your password',
reconnect: false
}
const toggleConnect = () => {
if (connected) {
dispatch(close());
} else {
dispatch(connect(details));
}
}
return (
<View style={styles.container}>
<Button onPress={toggleConnect} title={connected ? 'Disconnect' : 'Connect'} />
<Text>{connected ? counter.loaded ? `Counter: ${counter.value}` : 'Loading' : 'Not connected'}</Text>
</View>
);
}
If you run the app now, you will be able to see how connecting to Diffusion will result in a state transition that goes from 'Loading' to the providing the counter's value. In order to ensure that the application only subscribes when connected, the useEffect hook guarantees that the subscribe action is called only when the connection state changes (either due to deliberate opening/closing of a session, or disconnection because the application was backgrounded).
Unfortunately, right now the counter value will only show the default value of 0. To start seeing live updates, let's add add the ability to increment the counter topic.
Updating topics
Updating topics in Diffusion is also an asynchronous operation - which again maps neatly to Redux Thunk actions. In actions.js, add the following action to the end of the file:
export const incrementCounter = () => {
return async (dispatch, getState) => {
const { counter, connected, session } = getState();
if (session && connected) {
const specification = new topics.TopicSpecification(topics.TopicType.JSON);
const update = {
...counter,
value: counter.value + 1,
last_updated: Date.now()
};
return session.topicUpdate.set('counter', datatypes.json(), update, {
specification
});
}
throw new Error('Unable to publish when not connected');
}
}
This action simply updates a single topic, referencing the local state (synchronised by Diffusion) to increment the current value. The only thing left to do is call it from your app component. Import the new action at the top of App.js:
import { incrementCounter } from './store/diffusion/actions';
export function App () {
const connected = useSelector(state => state.connected);
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
useEffect(() => {
if (connected) {
dispatch(subscribeToCounter());
}
}, [connected]);
const details = {
host: 'Your Diffusion hostname',
principal: 'Your username',
credentials: 'Your password',
reconnect: false
}
const toggleConnect = () => {
if (connected) {
dispatch(close());
} else {
dispatch(connect(details));
}
}
const increment = () => { dispatch(incrementCounter()); }
return (
<View style={styles.container}>
<Button onPress={toggleConnect} title={connected ? 'Disconnect' : 'Connect'} />
<Text>{connected ? counter.loaded ? `Counter: ${counter.value}` : 'Loading' : 'Not connected'}</Text>
{connected ? (<Button onPress={increment} title='Increment'>) : (<></>)} </View>
);
}
If you run the application now in multiple windows / devices, you will be able to see how you can increment the counter and have your changes immediately replicated across for every connected user. You'll also be observe how the application correctly reflects changes in the background Diffusion connection state - such as when you background the app.
We hope that these examples of using Diffusion with Redux & Redux Thunk will help you build your own reliable, real-time applications with React and Diffusion!
Comments
0 comments
Please sign in to leave a comment.