├── .canvas ├── .github └── workflows │ └── canvas-sync-html.yml ├── .gitignore ├── .learn ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── __tests__ ├── CatList.test.js ├── Cats.test.js └── catsSlice.test.js ├── features └── cats │ ├── CatList.js │ ├── Cats.js │ └── catsSlice.js ├── index.js ├── reducer.js └── store.js /.canvas: -------------------------------------------------------------------------------- 1 | --- 2 | :lessons: 3 | - :id: 126576 4 | :course_id: 4182 5 | :canvas_url: https://learning.flatironschool.com/courses/4182/assignments/126576 6 | :type: assignment 7 | - :id: 166223 8 | :course_id: 5492 9 | :canvas_url: https://learning.flatironschool.com/courses/5492/assignments/166223 10 | :type: assignment 11 | -------------------------------------------------------------------------------- /.github/workflows/canvas-sync-html.yml: -------------------------------------------------------------------------------- 1 | # Secret stored in learn-co-curriculum Settings/Secrets 2 | 3 | name: Sync with Canvas HTML 4 | 5 | on: 6 | push: 7 | branches: [master, main] 8 | paths: 9 | - "README.md" 10 | 11 | jobs: 12 | sync: 13 | name: Sync with Canvas 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: 2.7 24 | 25 | - name: Install github-to-canvas 26 | run: gem install github-to-canvas 27 | 28 | - name: Sync from .canvas file 29 | run: github-to-canvas -a -lr --forkable --contains-html 30 | env: 31 | CANVAS_API_KEY: ${{ secrets.CANVAS_API_KEY }} 32 | CANVAS_API_PATH: ${{ secrets.CANVAS_API_PATH }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Learn-specific .results.json 26 | .results.json 27 | 28 | # Ignore ESLint files 29 | .eslintcache -------------------------------------------------------------------------------- /.learn: -------------------------------------------------------------------------------- 1 | --- 2 | languages: [] 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Toolkit 2 | 3 | ## Learning Goals 4 | 5 | - Use Redux Toolkit to simplify Redux setup and help follow best practices 6 | 7 | ## Introduction 8 | 9 | As we've been writing Redux code, we've added a pretty significant amount of 10 | complexity to our applications for managing state. For the apps we've been 11 | building in labs, this amount of complexity certainly may feel like overkill - 12 | we could just as easily have used `useState` and called it a day! As 13 | applications grow, having a consistent, predictable pattern for managing state 14 | will be beneficial. 15 | 16 | **However**, as we've seen, adding more state means adding more "boilerplate" 17 | code, such as: 18 | 19 | - Creating new reducers 20 | - Handling state immutably in our reducers 21 | - Adding new action creators 22 | 23 | We also need to go through a good amount of setup just to get Redux up and 24 | running: 25 | 26 | - Combine our reducers 27 | - Configure the Redux Dev Tools 28 | - Create our store 29 | - Add the `redux-thunk` middleware for async actions 30 | 31 | The amount of boilerplate code to get Redux up and running, and add new 32 | features, has been a consistent pain point for developers. Thankfully, the Redux 33 | team now has a tool to simplify the setup and make our job a bit easier: **Redux 34 | Toolkit**. We're going to work on refactoring an application that fetches data 35 | from an API to see how using Redux Toolkit can help simplify our code. 36 | 37 | To get started, install Redux Toolkit: 38 | 39 | ```console 40 | $ npm install @reduxjs/toolkit 41 | ``` 42 | 43 | Then, code along as we refactor. 44 | 45 | ## Store Setup 46 | 47 | Currently, our store setup looks like this: 48 | 49 | ```js 50 | // src/store.js 51 | import { createStore, applyMiddleware } from "redux"; 52 | import thunkMiddleware from "redux-thunk"; 53 | import { composeWithDevTools } from "redux-devtools-extension"; 54 | import rootReducer from "./reducer"; 55 | 56 | const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware)); 57 | 58 | const store = createStore(rootReducer, composedEnhancer); 59 | 60 | export default store; 61 | ``` 62 | 63 | We're also creating a root reducer in a separate file using `combineReducers`, 64 | so that we can add more reducers as our need for state grows: 65 | 66 | ```js 67 | // src/reducer.js 68 | import { combineReducers } from "redux"; 69 | import catsReducer from "./features/cats/catsSlice"; 70 | 71 | const rootReducer = combineReducers({ 72 | cats: catsReducer, 73 | }); 74 | 75 | export default rootReducer; 76 | ``` 77 | 78 | As you by now are surely aware, it takes quite a bit of work to get all the 79 | tools we need (`combineReducers`, `redux-thunk`, the Redux DevTools, etc.) all 80 | in place! Let's see how this setup looks with the Redux Toolkit instead: 81 | 82 | ```js 83 | // src/store.js 84 | import { configureStore } from "@reduxjs/toolkit"; 85 | 86 | import catsReducer from "./features/cats/catsSlice"; 87 | 88 | const store = configureStore({ 89 | reducer: { 90 | cats: catsReducer, 91 | }, 92 | }); 93 | 94 | export default store; 95 | ``` 96 | 97 | This one `configureStore` function does all the work we'd done by hand to set up 98 | our store and greatly simplifies it. It handles the work of: 99 | 100 | - Combining the reducers (we can just add other reducers in the `configureStore` 101 | function!); 102 | - Setting up `redux-thunk` (which is installed automatically as a dependency of 103 | Redux Toolkit); and 104 | - Adding the Redux DevTools! 105 | 106 | If you run `npm test` now, you should be able to confirm all the functionality 107 | we had previously set up by hand still works! 108 | 109 | One other benefit we get from the Redux toolkit is automatic checks for bugs 110 | around mutating state in our reducers. 111 | 112 | In our reducer, let's introduce a bug by mutating state (for demo purposes only, 113 | of course): 114 | 115 | ```js 116 | // src/features/cats/catsSlice.js 117 | export default function catsReducer(state = initialState, action) { 118 | switch (action.type) { 119 | case "cats/fetchCats/pending": 120 | // mutating state! nonono 121 | state.status = "loading"; 122 | return state; 123 | ``` 124 | 125 | If you run `npm start` and run our app in the browser, you should now get a 126 | nice, big error message in the console warning you about not mutating state in 127 | the reducer. This is an excellent error to have pop up in our applications - 128 | bugs related to improperly mutating state are notoriously difficult to spot, and 129 | can introduce a lot of strange behavior into our apps. Having this automatic 130 | check in place should give us more confidence that we're writing our reducer 131 | code properly! 132 | 133 | Now that we're done with the Redux Toolkit setup for our store, we can also now 134 | safely remove some dependencies from our app (since they're included with Redux 135 | Toolkit): 136 | 137 | ```console 138 | $ npm uninstall redux redux-thunk 139 | ``` 140 | 141 | ## Creating Slices 142 | 143 | Let's turn our attention next to our reducer and action creator code. All our 144 | code is in the `src/features/cats/catsSlice.js` file (a few new actions have 145 | been added for demo purposes). Let's start with the reducer: 146 | 147 | ```js 148 | // src/features/cats/catsSlice.js 149 | const initialState = { 150 | entities: [], // array of cats 151 | status: "idle", // loading state 152 | }; 153 | 154 | function catsReducer(state = initialState, action) { 155 | switch (action.type) { 156 | // sync actions 157 | case "cats/catAdded": 158 | return { 159 | ...state, 160 | entities: [...state.entities, action.payload], 161 | }; 162 | case "cats/catRemoved": 163 | return { 164 | ...state, 165 | entities: state.entities.filter((cat) => cat.id !== action.payload), 166 | }; 167 | case "cats/catUpdated": 168 | return { 169 | ...state, 170 | entities: state.entities.map((cat) => 171 | cat.id === action.payload.id ? action.payload : cat 172 | ), 173 | }; 174 | 175 | // async actions 176 | case "cats/fetchCats/pending": 177 | return { 178 | ...state, 179 | status: "loading", 180 | }; 181 | case "cats/fetchCats/fulfilled": 182 | return { 183 | ...state, 184 | entities: action.payload, 185 | status: "idle", 186 | }; 187 | 188 | default: 189 | return state; 190 | } 191 | } 192 | 193 | export default catsReducer; 194 | ``` 195 | 196 | One of the key requirements of our reducer is that we must always **return a new 197 | version of state**, and **never** mutate state. We're using the spread operator 198 | and a few tricks with different array methods to accomplish this. Let's see how 199 | we could simplify this with Redux Toolkit. 200 | 201 | To start off, we'll need to import the `createSlice` function: 202 | 203 | ```js 204 | import { createSlice } from "@reduxjs/toolkit"; 205 | ``` 206 | 207 | Then, we can update our reducer code like so: 208 | 209 | ```js 210 | const initialState = { 211 | entities: [], // array of cats 212 | status: "idle", // loading state 213 | }; 214 | 215 | const catsSlice = createSlice({ 216 | name: "cats", 217 | initialState, 218 | reducers: { 219 | catAdded(state, action) { 220 | // using createSlice lets us mutate state! 221 | state.entities.push(action.payload); 222 | }, 223 | catUpdated(state, action) { 224 | const cat = state.entities.find((cat) => cat.id === action.payload.id); 225 | cat.url = action.payload.url; 226 | }, 227 | // async actions to come... 228 | }, 229 | }); 230 | 231 | export default catsSlice.reducer; 232 | ``` 233 | 234 | Running `npm test` now after swapping out our reducer should still pass for all 235 | tests except those related to our _async_ actions (more on that later). 236 | 237 | One thing you'll notice is that we're now allowed to mutate state - no more 238 | spread operator! Under the hood, Redux Toolkit uses a library called [Immer][] 239 | to handle immutable state updates. We can safely write code that mutates state, 240 | as long as we're using `createSlice`, and Immer will ensure that we're not 241 | _actually_ mutating state. 242 | 243 | [immer]: (https://immerjs.github.io/immer/docs/introduction) 244 | 245 | Using `createSlice` will _also_ generate our action creators automatically! 246 | Let's delete the `catAdded` and `catUpdated` action creators we wrote by hand, 247 | and replace them with the ones generated by `createSlice`: 248 | 249 | ```js 250 | // the `catsSlice` object will have an `actions` property 251 | // with the auto-generated action creators 252 | export const { catAdded, catUpdated } = catsSlice.actions; 253 | ``` 254 | 255 | ## Async Action Creators 256 | 257 | Redux Toolkit also gives us another way to work with _async_ action creators 258 | using `redux-thunk`. We'll have to do a bit more work here to get these working 259 | than with our normal, non-thunk action creators creators. 260 | 261 | First, we'll need to import another function from Redux Toolkit: 262 | 263 | ```js 264 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 265 | ``` 266 | 267 | Then, we can use this `createAsyncThunk` function to create our `fetchCats` 268 | function: 269 | 270 | ```js 271 | export const fetchCats = createAsyncThunk("cats/fetchCats", () => { 272 | // return a Promise containing the data we want 273 | return fetch("https://learn-co-curriculum.github.io/cat-api/cats.json") 274 | .then((response) => response.json()) 275 | .then((data) => data.images); 276 | }); 277 | ``` 278 | 279 | Next, to add this to our reducer: 280 | 281 | ```js 282 | const catsSlice = createSlice({ 283 | name: "cats", 284 | initialState, 285 | reducers: { 286 | // sync reducers here 287 | }, 288 | // add this as a new key 289 | extraReducers: { 290 | // handle async action types 291 | [fetchCats.pending](state) { 292 | state.status = "loading"; 293 | }, 294 | [fetchCats.fulfilled](state, action) { 295 | state.entities = action.payload; 296 | state.status = "idle"; 297 | }, 298 | }, 299 | }); 300 | ``` 301 | 302 | To recap what the code above is doing: 303 | 304 | - We created a new async action creator using `createAsyncThunk`, called 305 | `fetchCats` 306 | - We added a new key on the slice object called `extraReducers`, where we can 307 | add custom reducer logic 308 | - We added a case in `extraReducers` for the `fetchCats.pending` state, which 309 | will run when our fetch request has not yet come back with a response 310 | - We also added a case for `fetchCats.fulfilled`, which will run when our 311 | response comes back with the cat data 312 | 313 | There's a lot to take in there! Working with async actions is still challenging, 314 | but using this approach at least gives us a consistent way to structure our 315 | async code and reduce the amount of hand-written logic for dealing with various 316 | fetch statuses ('idle', 'loading', 'error'). 317 | 318 | Here's what our completed slice file looks like after all those changes: 319 | 320 | ```js 321 | // src/features/cats/catsSlice.js 322 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 323 | 324 | export const fetchCats = createAsyncThunk("cats/fetchCats", () => { 325 | // return a Promise containing the data we want 326 | return fetch("https://learn-co-curriculum.github.io/cat-api/cats.json") 327 | .then((response) => response.json()) 328 | .then((data) => data.images); 329 | }); 330 | 331 | const catsSlice = createSlice({ 332 | name: "cats", 333 | initialState: { 334 | entities: [], // array of cats 335 | status: "idle", // loading state 336 | }, 337 | reducers: { 338 | catAdded(state, action) { 339 | // using createSlice lets us mutate state! 340 | state.entities.push(action.payload); 341 | }, 342 | catUpdated(state, action) { 343 | const cat = state.entities.find((cat) => cat.id === action.payload.id); 344 | cat.url = action.payload.url; 345 | }, 346 | }, 347 | extraReducers: { 348 | // handle async actions: pending, fulfilled, rejected (for errors) 349 | [fetchCats.pending](state) { 350 | state.status = "loading"; 351 | }, 352 | [fetchCats.fulfilled](state, action) { 353 | state.entities = action.payload; 354 | state.status = "idle"; 355 | }, 356 | }, 357 | }); 358 | 359 | export const { catAdded, catUpdated } = catsSlice.actions; 360 | 361 | export default catsSlice.reducer; 362 | ``` 363 | 364 | Running the tests again should still give you a passing result - meaning that 365 | our refactor was successful. 366 | 367 | You can see the full, working code in the solution branch. 368 | 369 | ## Conclusion 370 | 371 | Using Redux Toolkit can help remove a lot of the "boilerplate" setup code for 372 | working with Redux. It can also help save us from some of the common pitfalls of 373 | working with Redux, such as mutating state. Finally, it also gives us a way to 374 | structure our async code so that we can handle various loading states 375 | consistently and predictably. 376 | 377 | ## Resources 378 | 379 | - [Redux Toolkit](https://redux-toolkit.js.org/introduction/quick-start) 380 | - [Redux Toolkit: Advanced Tutorial](https://redux-toolkit.js.org/tutorials/advanced-tutorial) 381 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Building Forms Lab 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 | Loading! 15 |

16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-redux-thunk-lab", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.5.0", 7 | "bootstrap": "^4.5.3", 8 | "react": "^17.0.1", 9 | "react-dom": "^17.0.1", 10 | "react-redux": "^7.2.2", 11 | "react-scripts": "^5.0.1", 12 | "redux-devtools-extension": "^2.13.8" 13 | }, 14 | "devDependencies": { 15 | "@ihollander/jest-learn-reporter": "^1.0.1", 16 | "@testing-library/jest-dom": "^5.11.4", 17 | "@testing-library/react": "^11.1.0", 18 | "@testing-library/user-event": "^12.1.10", 19 | "fetch-mock": "^9.11.0", 20 | "mocha": "^8.2.1", 21 | "node-fetch": "^3.0.0", 22 | "redux-mock-store": "^1.5.4" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "echo \".results.json\" && react-scripts test --reporters=@ihollander/jest-learn-reporter --reporters=default --watchAll", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co-curriculum/react-hooks-redux-toolkit/f1dfa449821ad1c24f148679a345e773f5373318/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co-curriculum/react-hooks-redux-toolkit/f1dfa449821ad1c24f148679a345e773f5373318/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learn-co-curriculum/react-hooks-redux-toolkit/f1dfa449821ad1c24f148679a345e773f5373318/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Cats from "./features/cats/Cats"; 3 | 4 | function App() { 5 | return ; 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /src/__tests__/CatList.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import CatList from "../features/cats/CatList"; 4 | 5 | const catPics = [ 6 | { id: 1, url: "www.example.com/cat1" }, 7 | { id: 2, url: "www.example.com/cat2" }, 8 | ]; 9 | 10 | test("renders each cat pic in a tag with an alt prop of 'cat'", function () { 11 | render(); 12 | expect(screen.queryAllByAltText("cat")).toHaveLength(2); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/Cats.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { Provider } from "react-redux"; 4 | import { configureStore } from "@reduxjs/toolkit"; 5 | 6 | import catsReducer from "../features/cats/catsSlice"; 7 | import Cats from "../features/cats/Cats"; 8 | 9 | const catPics = [ 10 | { id: 1, url: "www.example.com/cat1" }, 11 | { id: 2, url: "www.example.com/cat2" }, 12 | ]; 13 | 14 | beforeEach(() => { 15 | const store = configureStore({ 16 | reducer: { 17 | cats: catsReducer, 18 | }, 19 | }); 20 | store.dispatch({ type: "cats/fetchCats/fulfilled", payload: catPics }); 21 | 22 | render( 23 | 24 | 25 | 26 | ); 27 | }); 28 | 29 | test("passes catPics from the store down as a prop to CatList", () => { 30 | expect(screen.queryAllByAltText("cat")).toHaveLength(2); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/catsSlice.test.js: -------------------------------------------------------------------------------- 1 | import { waitFor } from "@testing-library/react"; 2 | import configureMockStore from "redux-mock-store"; 3 | import thunk from "redux-thunk"; 4 | import fetchMock from "fetch-mock"; 5 | import catsReducer, { 6 | fetchCats, 7 | catAdded, 8 | catUpdated, 9 | } from "../features/cats/catsSlice"; 10 | 11 | const middlewares = [thunk]; 12 | const mockStore = configureMockStore(middlewares); 13 | 14 | const catPics = [ 15 | { url: "www.example.com/cat1" }, 16 | { url: "www.example.com/cat2" }, 17 | ]; 18 | 19 | describe("async actions", () => { 20 | afterEach(() => { 21 | fetchMock.restore(); 22 | }); 23 | 24 | test('creates an async action object with type of "cats/catsLoaded" and a payload of cat images', async () => { 25 | fetchMock.getOnce( 26 | "https://learn-co-curriculum.github.io/cat-api/cats.json", 27 | { 28 | body: { 29 | images: catPics, 30 | }, 31 | headers: { "content-type": "application/json" }, 32 | } 33 | ); 34 | 35 | const expectedActions = [ 36 | { type: "cats/fetchCats/pending" }, 37 | { type: "cats/fetchCats/fulfilled", payload: catPics }, 38 | ]; 39 | 40 | const store = mockStore({}); 41 | await store.dispatch(fetchCats()); 42 | 43 | waitFor(() => { 44 | expect(store.getActions()).toEqual(expectedActions); 45 | }); 46 | }); 47 | }); 48 | 49 | describe("sync actions", () => { 50 | test("catAdded() returns the correct object", () => { 51 | expect(catAdded({ id: 1, url: "www.example.com/cat1" })).toEqual({ 52 | type: "cats/catAdded", 53 | payload: { id: 1, url: "www.example.com/cat1" }, 54 | }); 55 | }); 56 | test("catUpdated() returns the correct object", () => { 57 | expect(catUpdated({ id: 1, url: "www.example.com/cat2" })).toEqual({ 58 | type: "cats/catUpdated", 59 | payload: { id: 1, url: "www.example.com/cat2" }, 60 | }); 61 | }); 62 | }); 63 | 64 | describe("catsReducer()", () => { 65 | test("returns the initial state", () => { 66 | expect(catsReducer(undefined, {})).toEqual({ 67 | status: "idle", 68 | entities: [], 69 | }); 70 | }); 71 | 72 | test("handles the 'cats/catAdded' action", () => { 73 | expect( 74 | catsReducer(undefined, { 75 | type: "cats/catAdded", 76 | payload: { id: 1, url: "www.example.com/cat1" }, 77 | }) 78 | ).toEqual({ 79 | status: "idle", 80 | entities: [{ id: 1, url: "www.example.com/cat1" }], 81 | }); 82 | }); 83 | 84 | test("handles the 'cats/catUpdated' action", () => { 85 | expect( 86 | catsReducer( 87 | { 88 | status: "idle", 89 | entities: [{ id: 1, url: "www.example.com/cat1" }], 90 | }, 91 | { 92 | type: "cats/catUpdated", 93 | payload: { id: 1, url: "www.example.com/cat2" }, 94 | } 95 | ) 96 | ).toEqual({ 97 | status: "idle", 98 | entities: [{ id: 1, url: "www.example.com/cat2" }], 99 | }); 100 | }); 101 | 102 | test("handles the 'cats/fetchCats/pending' action", () => { 103 | expect( 104 | catsReducer(undefined, { 105 | type: "cats/fetchCats/pending", 106 | }) 107 | ).toEqual({ status: "loading", entities: [] }); 108 | }); 109 | 110 | test("handles the 'cats/fetchCats/fulfilled' action", () => { 111 | const catPics = [ 112 | { url: "www.example.com/cat1" }, 113 | { url: "www.example.com/cat2" }, 114 | ]; 115 | expect( 116 | catsReducer(undefined, { 117 | type: "cats/fetchCats/fulfilled", 118 | payload: catPics, 119 | }) 120 | ).toEqual({ status: "idle", entities: catPics }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/features/cats/CatList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function CatList({ catPics = [] }) { 4 | return ( 5 |
6 | {catPics.map((pic) => ( 7 | cat 8 | ))} 9 |
10 | ); 11 | } 12 | 13 | export default CatList; 14 | -------------------------------------------------------------------------------- /src/features/cats/Cats.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import CatList from "./CatList"; 4 | import { fetchCats } from "./catsSlice"; 5 | 6 | function Cats() { 7 | const catPics = useSelector((state) => state.cats.entities); 8 | 9 | const dispatch = useDispatch(); 10 | 11 | useEffect(() => { 12 | dispatch(fetchCats()); 13 | }, [dispatch]); 14 | 15 | return ( 16 |
17 |

CatBook

18 | 19 |
20 | ); 21 | } 22 | 23 | export default Cats; 24 | -------------------------------------------------------------------------------- /src/features/cats/catsSlice.js: -------------------------------------------------------------------------------- 1 | // Action Creators 2 | 3 | // async actions 4 | export function fetchCats() { 5 | return function (dispatch) { 6 | dispatch({ type: "cats/fetchCats/pending" }); 7 | fetch("https://learn-co-curriculum.github.io/cat-api/cats.json") 8 | .then((response) => response.json()) 9 | .then((data) => { 10 | dispatch({ 11 | type: "cats/fetchCats/fulfilled", 12 | payload: data.images, 13 | }); 14 | }); 15 | }; 16 | } 17 | 18 | // sync actions added for demo purposes 19 | export function catAdded(newCat) { 20 | return { 21 | type: "cats/catAdded", 22 | payload: newCat, 23 | }; 24 | } 25 | 26 | export function catUpdated(updatedCat) { 27 | return { 28 | type: "cats/catUpdated", 29 | payload: updatedCat, 30 | }; 31 | } 32 | 33 | // Reducer 34 | const initialState = { 35 | entities: [], // array of cats 36 | status: "idle", // loading state 37 | }; 38 | 39 | function catsReducer(state = initialState, action) { 40 | switch (action.type) { 41 | // sync actions 42 | case "cats/catAdded": 43 | return { 44 | ...state, 45 | entities: [...state.entities, action.payload], 46 | }; 47 | case "cats/catUpdated": 48 | return { 49 | ...state, 50 | entities: state.entities.map((cat) => 51 | cat.id === action.payload.id ? action.payload : cat 52 | ), 53 | }; 54 | 55 | // async actions 56 | case "cats/fetchCats/pending": 57 | return { 58 | ...state, 59 | status: "loading", 60 | }; 61 | case "cats/fetchCats/fulfilled": 62 | return { 63 | ...state, 64 | entities: action.payload, 65 | status: "idle", 66 | }; 67 | 68 | default: 69 | return state; 70 | } 71 | } 72 | 73 | export default catsReducer; 74 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import "bootstrap/dist/css/bootstrap.min.css"; 5 | import App from "./App"; 6 | import store from "./store"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import catsReducer from "./features/cats/catsSlice"; 3 | 4 | const rootReducer = combineReducers({ 5 | cats: catsReducer, 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | 3 | import catsReducer from "./features/cats/catsSlice"; 4 | 5 | const store = configureStore({ 6 | reducer: { 7 | cats: catsReducer, 8 | }, 9 | }); 10 | 11 | export default store; 12 | --------------------------------------------------------------------------------