Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Redux Toolkit patterns including slices, selectors, thunks, and middleware conventions for Trezor Suite. Use when writing or reviewing Redux state management code.
.claude/skills/trezor-redux/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 111% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 43% | 0% |
We use redux-toolkit for writing all of redux code. It has functions that build in suggested best practices, including setting up the store to catch mutations and enable the Redux DevTools Extension, simplifying immutable update logic with Immer, and more.
Co-locating logic for a given feature in one place typically makes it easier to maintain that code. This is also known as the "ducks" pattern. While older Redux codebases often used a "file-by-type" approach with separate folders for "actions" and "reducers", keeping related logic together makes it easier to find and update that code.
This Single-File approach is strongly recommended by official Redux Style Guide and offers many benefits over a "file-by-type" structure. In our case, in our monorepo, we do something similar but with packages and folders. Usually we have one package per feature or in some specific cases we have packages where you have multiple feature folders. Benefits of this approach:
jsx// good myPackage / myPackageReducer.ts; myPackageActions.ts; myPackageThunks.ts; // good myPackage / myPackageReducer.ts; myPackageActions.ts; myPackageThunks.ts; myPackageSelectors.ts;
tsx import { name as packageName } from './package.json';
const sliceName = 'appSettings'; const actionPrefix = ${packageName}/${sliceName};
${sliceName}State.tsx export interface AppSettingsState { colorScheme: AppColorScheme; fiatCurrency: 'czk' | 'usd'; }
${sliceName}RootState.tsx type AppSettingsRootState = { appSettings: AppSettingsState; };
createAction instead of using createSlice generated actions, you should place them here.tsx export const doSomeMagic = createAction(${actionPrefix}/doSomeMagic);
createSlice function.tsx const appSettingsSlice = createSlice({ name: 'appSettings', initialState, reducers: { setColorScheme: (state, action: PayloadAction<AppColorScheme>) => { state.colorScheme = action.payload; }, }, });
Keep the slice object private. Export its actions and reducer as named values. For slices created with createSliceWithExtraDeps, export prepareReducer instead of the prepared reducer. Define selectors as standalone exported functions so their declarations do not expose generated slice or Reselect implementation types.
Explicitly type the state parameter of every case reducer. This keeps generated declaration files from exposing Redux Toolkit's inferred slice implementation types.
tsxconst appSettingsSlice = createSlice({ name: 'appSettings', initialState, reducers: { setColorScheme(state: AppSettingsState, action: PayloadAction<AppColorScheme>) { state.colorScheme = action.payload; }, }, }); export const appSettingsActions = appSettingsSlice.actions; export const appSettingsReducer = appSettingsSlice.reducer; export const selectColorScheme = (state: AppSettingsRootState) => state.appSettings.colorScheme;
Do not export appSettingsSlice. Consumers should not depend on the complete inferred slice API.
Sources:
Prefer using predefined selectors to access state. Doing this simplifies refactoring in future. Inline selectors are ok for less used properties - so that we do not have to write helpers for every existing property.
tsx// bad const transactions = useSelector(state => state.wallet.transactions[accountKey]); const language = useSelector(state => state.settings.language); // good const transactions = useSelector(state => selectTransactions(state, accountKey)); const language = useSelector(selectLanguage); // Thunks const myThunk = createThunk('myThunk', ({ accountKey }, { getState }) => { // bad const transactions = getState().wallet.transactions[accountKey]; // good const transactions = selectTransactions(getState(), accountKey); });
If you decide to refactor, for example, the whole transaction data structure in the reducer state, you won't need to make changes in every place where it's accessed. The only place where you will need to make changes is that selector. You won't need to go over all components where you are accessing transactions. That's a huge benefit 🎉
Try to step back from making useSelector hooks return an object. It has no benefits and might cause performance issues. Use one useSelector per value.
tsx// bad const { myValue, myAnotherValue } = useSelector(state => ({ myValue: state.something.myValue, myAnotherValue: state.something.myAnotherValue }); // good const myValue = useSelector(selectMyValue); const myAnotherValue = useSelector(selectMyAnotherValue);
Always prefix selectors with select and when you use them drop select. When selecting by a parameter, suffix the name like this: selectAccountById .
tsx// bad const getAccount = ...; const findAccount = ...; const getAccountByKey ...; // good const selectAccount = ...; const selectAccountByKey = ...; // bad const userAccount = useSelector(selectAccount); const oldAccount = useSelector(selectAccount); const foundAccount = useSelector(selectAccountByKey(key)); // good const account = useSelector(selectAccount); const account = useSelector(selectAccountByKey(key));
createAction for creating actions related to reducers which have not been converted to slices yet.createThunk() methodThunk postfix, e.g. connectInitThunk()npm-module-or-app/reducer/ACTION_TYPE for naming a thunk. Each slice should have an actionPrefix defined in constants.tstsxexport const actionPrefix = '@common/wallet-core/accounts' const disableAccountsThunk = createThunk( `${actionPrefix}/disableAccountsThunk`, ......
const state = getState() causing bugs because it will use an old snapshot of the state, use getState() directly when needed, e.g.tsxawait TrezorConnect.init({ ...connectInitSettings, pendingTransportEvent: selectIsPendingTransportEvent(getState()), });
Avoid usage of const state = getState() because assigning result of getState() to variable will create snapshot of state at a given moment and could lead to unintentionally accessing some old version of state. For example:
tsxcreateMiddleware((action, { next }) => { const state = getState(); next(action); // this will dispatch action and change state // you are still accessing old version of state before change console.log(state); // this will always access current version of state console.log(getState()); });
This is something that could lead to hard-to-debug bugs, but sometimes you want to preserve previous version of state on purpose. In that case, avoid naming it just state but prefer something like prevState which will prevent anyone from thinking that it has an actual state.
Middlewares should be read-only - they should not dispatch actions or modify state. Otherwise, they produce code that is hard to read and test that leads to nasty bugs.
Other measured skills in the registry, with their headline benchmark lift.