Skip to content

v.1.3.0-alpha.0

Pre-release
Pre-release
Compare
Choose a tag to compare
@markerikson markerikson released this 12 Feb 06:17

This release adds two new APIs: createEntityAdapter to help manage normalized state, and createAsyncThunk to abstract common data fetching behavior.

Note: this is an alpha release. These APIs are currently minimally tested, and the implementation details and API signatures may change. We hope that these APIs will be useful enough to officially release in the near future, and encourage users to try them out and give us feedback.

Please comment in issue #76: Create Async Action, issue #333: Consider adding logic for normalized state, and PR #352: Port ngrx/entity and add createAsyncThunk and let us know your thoughts!

This version is available as @reduxjs/toolkit@alpha on NPM, or @reduxjs/toolkit@1.3.0-alpha.0.

Changes

createEntityAdapter

The Redux docs have long advised storing data in a "normalized" state shape, which typically means keeping each type of item in a structure that looks like {ids: [], entities: {} }. However, the Redux core provides no APIs to help manage storing and updating your data using this approach. Many community libraries exist, with varying tradeoffs, but so far we haven't officially recommended any of them.

Caching data is a hard problem, and not one that we are interested in trying to solve ourselves. However, given that we do recommend this specific pattern, and that Redux Toolkit is intended to help simplify common use cases, we want to provide a minimal set of functionality to help users manage normalized state.

For this alpha release, we've specifically ported the @ngrx/entity library to work with Redux Toolkit, with some modifications.

The core API function is createEntityAdapter. It generates a set of reducer functions and selectors that know how to work with data that has been stored in that normalized {ids: [], entities: {} } format, and can be customized by passing in a function that returns the ID field for a given item. If you want to keep the item IDs in a sorted order, a comparison function can also be passed in.

The returned EntityAdapter object contains generated CRUD functions for manipulating items within that state, and generated selector functions that know how to read from that state. You can then use the generated CRUD functions and selectors within your own code.

Since this is an alpha, we don't have any API documentation yet. Please refer to the @ngrx/entity API docs for createEntityAdapter as a reference.

There is one very important difference between RTK's implementation and the original @ngrx/entity implementation. With @ngrx/entity, methods like addOne(item, state) accept the data argument first and the state second. With RTK, the argument order has been flipped, so that the methods look like addOne(state, item), and the methods can also accept a standard Redux Toolkit PayloadAction containing the data as the second argument. This allows them to be used as Redux case reducers directly, such as passing them in the reducers argument for createSlice.

Note: we've also updated these methods to use Immer internally. They already made immutable updates, but this simplified the implementation details. However, there is currently an issue we're seeing with nested uses of Immer behaving unexpectedly, so be careful when calling them inside a createSlice case reducer. Please see immerjs/immer#533 for details.

createAsyncThunk

The Redux docs have also taught that async logic should typically dispatch "three-phase async actions" while doing data fetching: a "start" action before the request is made so that loading UI can be displayed, and then a "success" or "failure" action to handle loading the data or showing an error message. Writing these extra action types is tedious, as is writing thunks that dispatch these actions and differ only by what the async request is.

Given that this is a very common pattern, we've added a createAsyncThunk API that abstracts this out. It accepts a base action type string and a callback function that returns a Promise as an argument, which is primarily intended to be a function that does a data fetch and returns a Promise containing the results. It then auto-generates the request lifecycle action types / creators, and generates a thunk that dispatches those lifecycle actions and runs the fetching callback.

From there, you can listen for those generated action types in your reducers, and handle loading state as desired.

Example Usage

This example demonstrates the basic intended usage of both createEntityAdapter and createAsyncThunk. It's incomplete, but hopefully shows enough of the idea to let you get started:

const usersAdapter = createEntityAdapter();

const fetchUsers = createAsyncThunk(
    "users/fetch",
    () => usersAPI.fetchAll()
);

// `fetchUsers` is now a typical thunk action creator, but also has
// four more action creators attached:
// pending, fulfilled, finished, rejected
// it will automatically dispatch those based on the promise lifecycle

const usersSlice = createSlice({
    name: "users",
    initialState: usersAdapter.getInitialState({loading: true}),
    reducers: {
        // createSlice will generate "users/userAdded" action types for these reducers
        userAdded: usersAdapter.addOne,
        userRemoved: usersAdapter.removeOne,
        // etc
    },
    extraReducers: {
        // would also want to handle the loading state cases, probably with a state machine,
        // using the lifecycle actions attached to fetchUsers
        [fetchUsers.fulfilled](state, action) {
            return usersAdapter.upsertMany(state, action.payload.result)
        }
    }
});