Code Structure
We recommend the following file structure for projects using ReModel. This structure ensures a clear separation of concerns, making your codebase more maintainable and scalable.
- Header.tsx
- Footer.tsx
- ModelProvider.tsx
- index.ts
- index.ts
- actions.ts
- index.ts
- selectors.ts
- epics.ts
- services.ts
- reducer.ts
- types.ts
Explanation of Directories and Files
-
src/app:App.tsx: The entry point for the application, where the main UI layout is defined.
-
src/components: Contains reusable UI components that can be shared across the application. -
src/providers:ModelProvider.tsx: A provider for integrating the model layer (e.g., Redux and RxJS) with the application, exposing context or hooks for accessing shared state or logic.index.ts: Serves as the entry point for the providers module, re-exporting relevant components or functions.
-
src/models:index.ts: The main entry point for the models directory, consolidating exports from all submodules.user:actions.ts: Defines actions related to theuserfeature, describing state transitions and events.index.ts: Serves as the entry point for theusersubmodule, exporting relevant parts like actions, selectors, and reducers.selectors.ts: Contains selector functions to retrieve specific slices of theuserstate.epics.ts: Manages RxJS-based epics for handling side effects like API calls.services.ts: Encapsulates logic for interacting with APIs or other external services.reducer.ts: Defines the reducer logic for theuserstate, managing how actions modify the state.types.ts: Contains TypeScript type definitions for theusermodule, ensuring strong typing throughout the codebase.
File Contents
Below are the code examples for the key files in the project.
App entry point
import React from 'react';
import { ReModelProvider } from '@blue-functor/remodel';
import model from '../models';
const App: React.FC = () => {
return (
<ReModelProvider model={model}>
<div style={{ textAlign: 'center' }}>
<main style={{ padding: '2rem' }}>
<p>This is the main application layout. Start building your app here!</p>
</main>
</div>
</ReModelProvider>
);
};
export default App;In your app’s entry point, you wrap the main layout with the ReModelProvider component, passing the model object as a prop. This setup ensures that the model layer is integrated with the application, allowing components to access shared state and logic.
models/index.ts
import { reducer as ui, epics as uiEpics } from './ui';
import { reducer as user, epics as userEpics } from './user';
export type RootState = {
ui: ReturnType<typeof ui>;
user: ReturnType<typeof user>;
};
export const models = {
reducers: {
ui,
user,
},
epics: {
uiEpics,
userEpics,
},
};The models/index.ts file serves as the entry point for the application’s model layer.
In this file, we create the models object, which combines all the reducers and epics in the application.
This object is passed to the ReModelProvider to integrate the Redux and RxJS layers into the application.
models/user/actions.ts
import { generateScopedActions } from '@blue-functor/remodel';
const { createAction, createEffectAction } = generateScopedActions('USER');
export const setIsLoading = createAction('SET_IS_LOADING');
export const signIn = createEffectAction<
{ username: string; password: string },
{ token: string }
>('SIGN_IN');This file defines all the actions related to the USER model using the generateScopedActions utility from @blue-functor/remodel.
The generateScopedActions function ensures that all actions created in this file are scoped with the prefix USER/,
providing a clear and consistent namespace.
This scoping helps avoid conflicts between action types across different models and makes debugging and tracing actions easier.
models/users/selectors.ts
import { RootState } from '../rootState';
export const userToken = (state: RootState) => state.user.token;
export const userDetails = (state: RootState) => state.user.details;
export const userLoading = (state: RootState) => state.user.loading;This file defines selectors for accessing specific parts of the USER state in the Redux store.
- Selectors: Functions like
userTokenanduserDetailsretrieve specific slices of theUSERstate from the globalRootState. For example: userTokenreturns the user’s token.userDetailsreturns detailed information about the user.
Selectors provide a clean and reusable way to access state, ensuring consistency and reducing duplication throughout the application.
models/users/reducer.ts
import { createReducer } from '@reduxjs/toolkit';
import * as actions from './actions';
const initialState = {
loading: false,
token: null,
details: null,
};
const reducer = createReducer(initialState, (builder) =>
builder
.addCase(actions.signIn, (state, { payload }) => {
state.loading = true;
})
.addCase(actions.signIn.succeeded, (state, { payload }) => {
state.token = payload.token;
state.details = payload.details;
state.loading = false;
})
.addCase(actions.signIn.failed, (state) => {
state.loading = false;
})
);
export default reducer;This file defines the reducer for managing the USER state in the Redux store. It uses @reduxjs/toolkit’s createReducer utility for a clean and concise syntax:
-
Initial State: The
initialStateobject defines the default structure and values of theUSERstate, such astokenanddetails. -
Reducers: The reducer listens to actions defined in
actions.ts:signIn: Handles the initial sign-in action, potentially updating state based on the payload.signInEffect.succeeded: Handles the successful result of a side effect (e.g., API response), updating thetokenanddetailsstate with the payload.
This pattern keeps the state management logic centralized and easy to maintain while leveraging action creators for consistency.
createReducer documentationmodels/users/epics.ts
import { combineEpics } from 'redux-observable';
import { ofType, Epic, effect } from '@blue-functor/remodel';
import * as actions from './actions';
const signInEpic: Epic = (action$) =>
action$.pipe(
ofType(actions.signIn),
effect(actions.signInEffect, ({ username, password }) =>
Promise.resolve({ token: 'dummyToken', details: { username, roles: ['user'] } })
),
);
export default combineEpics(signInEpic);This file defines the epics for handling side effects related to the USER model. It uses redux-observable and @blue-functor/remodel to streamline the management of asynchronous workflows:
-
signInEpic: -
Triggers when the
signInaction is dispatched. -
Uses the
effectoperator to simulate an API call, returning a mocktokenanddetails. -
Combining Epics:
-
The
combineEpicsfunction merges all individual epics into a single export, making it easy to integrate with the application’s middleware.
Epics provide a powerful mechanism for handling asynchronous logic and separating it from reducers, ensuring a clean and maintainable codebase.