State management and Redux
The Sharetribe Web Template is single-page application built using React. This means that the app needs to be able to render several different layouts (pages) depending on user interaction. State management is essential for this process. The template needs to know if a user has been authenticated or if it has received relevant data for the current page, and so on.
What is Redux
We use Redux for state management on the application level. You should read more about Redux before you start modifying queries to the Sharetribe API or creating new Page level elements.
The template follows the recommendation to pair Redux with Redux Toolkit . Redux Toolkit reduces the amount of repetition needed to use Redux and it simplifies most Redux tasks, minimizing the risk of mistakes, and making it easier to write Redux applications.
In the following subtopics, we assume that you know the basics of Redux already.
Container components
Container components in the Sharetribe Web Template are connected to the
Redux state store, while other components and forms are purely
presentational. This makes it easier to customize UI components as you
don’t need to be aware of the complexity related to Redux setup when you
just want to modify the behavior of some visual component. In those
cases, you can just head to the src/components/ directory and you can
see from there what props are available for each component when they are
rendered.
Naturally, there is a need for higher level components which fetch new
data and define what props are passed down to presentational components.
In Redux terminology, those components are called Containers. The
template has defined all the containers inside a directory called
src/containers/. It contains all the pages and a special container for
the top bar (TopbarContainer) since that is a very complex component and
it is shared with almost every page. You can read more about differences
between presentational and container components from this
article written by Dan Abramov .
The actual container setup of a page level component can be found from
the bottom of the component file. For example,
src/containers/TransactionPage/TransactionPage.js connects itself to
Redux store with mapStateToProps and mapDispatchToProps functions:
const TransactionPage = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TransactionPageComponent);Duck files
Inside any src/containers/<ComponentName> directory, you will also
find a <ComponentName>.duck.js file. These files wrap all the other
page-specific Redux concepts into a single file. With Redux Toolkit,
these files use createSlice and createAsyncThunk to define state
slices with their reducers and async operations in one place. This keeps
pages encapsulated - page specific state management, actions, and data
fetches happen inside their respective directory. Look for files whose
name follows the pattern: <ComponentName>.duck.js. This pattern
consolidates related Redux logic into a single file, originally inspired
by the
ducks modular redux pattern .
Global reducers
Some reducers are needed in several pages. These global reducers are
defined inside src/ducks/ directory with their respective *.duck.js
files. The most important global duck files are user.duck.js and
marketplaceData.duck.js.
Setting up Redux
Container specific reducers are gathered and exported inside
src/containers/reducers.js file and global reducers are exported
respectively in a file src/ducks/index.js.
With those exports, we are able to create appReducer
(src/reducers.js):
import { combineReducers } from 'redux';
import * as globalReducers from './ducks';
import * as pageReducers from './containers/reducers';
const appReducer = combineReducers({
...globalReducers,
...pageReducers,
});appReducer is called by createReducer function, which is called
inside the configureStore function (in src/store.js):
export default function configureStore({
initialState = {},
sdk = null,
analyticsHandlers = [],
extraMiddlewares = [],
}) {
const store = configureStoreReduxToolkit({
reducer: createReducer(),
preloadedState: initialState,
middleware: (getDefaultMiddleware) => {
const middlewares = getDefaultMiddleware({
thunk: {
extraArgument: sdk,
},
// Note: we do save class-based objects like UUIDs, Money, LatLng, LatLngBounds, Dates and Decimals to the store
serializableCheck: false,
}).prepend(
analytics.createAnalyticsListenerMiddleware(analyticsHandlers)
.middleware
);
return middlewares.concat(extraMiddlewares);
},
devTools: appSettings.dev && typeof window === 'object',
});
return store;
}This setup creates a store structure that separates container specific state as well as global data by their reducer names. Together with the Ducks module naming schema, this means that:
- the state of the
ListingPagecan be found fromstate.ListingPageand - the state of the global
userobject can be found fromstate.user.
Slices
In Redux Toolkit, state and reducers are defined in slices using
createSlice. Each slice represents a portion of the overall Redux
state and contains its initial state, reducers, and automatically
generated action creators.
A typical slice in the template is defined like this:
const emailVerificationSlice = createSlice({
name: 'emailVerification',
initialState: {
isVerified: false,
verificationError: null,
verificationInProgress: false,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(verifyEmail.pending, (state) => {
state.verificationInProgress = true;
state.verificationError = null;
})
.addCase(verifyEmail.fulfilled, (state) => {
state.verificationInProgress = false;
state.isVerified = true;
})
.addCase(verifyEmail.rejected, (state, action) => {
state.verificationInProgress = false;
state.verificationError = action.payload;
});
},
});Synchronous actions are defined under the reducers object, whereas
asynchronous thunks are listed under extraReducers.
Thunks
One essential part of state management in the template is filling the Redux store with data fetched from the Sharetribe API. This is done with Redux Thunks , which is a Redux middleware to create asynchronous action creators.
As with every other Redux store action, you can find Thunks inside
*.duck.js files. For example, fetching listing reviews can be done
with the following thunk function:
export const fetchReviewsThunk = createAsyncThunk(
'ListingPage/fetchReviews',
({ listingId }, { rejectWithValue, extra: sdk }) => {
return sdk.reviews
.query({
listing_id: listingId,
state: 'public',
include: ['author', 'author.profileImage'],
'fields.image': [
'variants.square-small',
'variants.square-small2x',
],
})
.then((response) => {
return denormalisedResponseEntities(response);
})
.catch((e) => {
return rejectWithValue(storableError(e));
});
}
);The thunk will usually be wrapped in a helper function (see example
below). This structure is to enable better backward compatibility with
versions of the template that predate Redux Toolkit. The unwrap
function returns a new Promise that either has the actual action.payload
value from a fulfilled action, or throws an error if it’s the rejected
action. This allows presentational components to handle success and
failure using try/catch logic.
export const fetchReviews =
(listingId) => (dispatch, getState, sdk) => {
return dispatch(fetchReviewsThunk({ listingId })).unwrap();
};The sdk parameter is provided as the extra argument in Redux Toolkit’s createAsyncThunk. We configure this in store.js by passing the SDK to the thunk middleware: thunk: { extraArgument: sdk }
Further reading
See a practical examples of how to write state managmenet logic in: