Skip to content

Commit

Permalink
Migrate away from redux
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Nov 22, 2024
1 parent d50cfd9 commit 4d403e4
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 296 deletions.
7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,13 @@
}
},
"dependencies": {
"@reduxjs/toolkit": "^2.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.0.4"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
"@storybook/addon-a11y": "^8.5.0-alpha.9",
"@storybook/addon-essentials": "^8.5.0-alpha.9",
"@storybook/blocks": "^8.5.0-alpha.9",
"@storybook/experimental-addon-test": "^8.5.0-alpha.9",
"@storybook/react": "^8.5.0-alpha.9",
"@storybook/react-vite": "^8.5.0-alpha.9",
Expand All @@ -63,9 +60,7 @@
"msw": "^2.3.0",
"msw-storybook-addon": "^2.0.3",
"playwright": "^1.49.0",
"prop-types": "^15.8.1",
"storybook": "^8.5.0-alpha.9",
"ts-migrate": "^0.1.35",
"typescript": "^5.6.3",
"vite": "^5.2.0",
"vitest": "^2.1.5"
Expand Down
6 changes: 1 addition & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import "./index.css";
import store from "./lib/store";

import { Provider } from "react-redux";
import InboxScreen from "./components/InboxScreen";
function App() {
return (
<Provider store={store}>
<InboxScreen />
</Provider>
<InboxScreen />
);
}

Expand Down
32 changes: 19 additions & 13 deletions src/components/InboxScreen.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { Meta, StoryObj } from '@storybook/react'
import { HttpResponse, http, delay } from 'msw'
import { Provider } from 'react-redux'
import MockDate from 'mockdate'

import store from '../lib/store'
import InboxScreen from './InboxScreen'
import * as mocks from '../mocks/data'

import {
userEvent,
waitFor,
within,
waitForElementToBeRemoved,
expect,
} from '@storybook/test'
import { getFormattedDate } from '#utils/date.mock.ts'

const meta = {
component: InboxScreen,
title: 'InboxScreen',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
} satisfies Meta<typeof InboxScreen>
export default meta;

Expand All @@ -41,17 +37,27 @@ export const Default: Story = {

export const PinnedTasks: Story = {
...Default,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
play: async ({ canvas, step }) => {
// Waits for the component to transition from the loading state
await waitForElementToBeRemoved(await canvas.findByTestId('loading'))
// Waits for the component to be updated based on the store
await waitFor(async () => {
// Simulates pinning the first task
await userEvent.click(canvas.getByLabelText('Pin Learn more about Storybook'))
// Simulates pinning the third task
await userEvent.click(canvas.getByLabelText('Pin Schedule annual health check-up'))

await step('Ensure tasks are rendered in the initial order', async () => {
const listItems = canvas.getAllByRole("listitem");
await expect(listItems[0]).toHaveTextContent("Learn more about Storybook");
await expect(listItems[1]).toHaveTextContent("Go to the gym");
})

await step('Pin "Go to the gym" task', async () => {
// Pin Learn more about Storybook and verify it moves to the top
const pinButton = canvas.getByLabelText("Pin Go to the gym");
await userEvent.click(pinButton);
});

await step('Ensure tasks order is changed', async () => {
const updatedListItems = canvas.getAllByRole("listitem");
await expect(updatedListItems[0]).toHaveTextContent("Go to the gym");
await expect(updatedListItems[1]).toHaveTextContent("Learn more about Storybook");
});
},
}

Expand Down
18 changes: 5 additions & 13 deletions src/components/InboxScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { useEffect, useMemo } from "react";
import { useMemo } from "react";

import { useDispatch, useSelector } from "../lib/hooks";
import { fetchTasks } from "../lib/store";
import { selectTaskbox } from "../lib/selectors";
import TaskList from "./TaskList";
import { getFormattedDate } from "#utils/date";
import { useTasks } from "#lib/useTasks.ts";

export default function InboxScreen() {
const dispatch = useDispatch();
// We're retrieving the error field from our updated store
const { error } = useSelector(selectTaskbox);
// The useEffect triggers the data fetching when the component is mounted
useEffect(() => {
dispatch(fetchTasks());
}, []);

const { error, tasks, updateTaskState, status } = useTasks();

const today = useMemo(() => getFormattedDate(new Date()), []);

if (error) {
Expand All @@ -33,7 +25,7 @@ export default function InboxScreen() {
<nav>
<h1 className="title-page">Taskbox - {today}</h1>
</nav>
<TaskList />
<TaskList tasks={tasks} updateTaskState={updateTaskState} status={status}/>
</div>
);
}
126 changes: 28 additions & 98 deletions src/components/TaskList.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,17 @@
import type { ReactElement } from 'react';
import type { Meta, StoryObj } from '@storybook/react'
import { Provider } from "react-redux";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { expect, fn, userEvent } from '@storybook/test';

import { UseTasksPayload } from '#lib/useTasks.ts';
import TaskList from "./TaskList";
import { State } from '../lib/store';
import { Task } from '../types';
import * as mocks from "../mocks/data";
import { expect, userEvent } from '@storybook/test';

// A super-simple mock of the state of the store
const defaultTaskboxState: State = {
const defaultTaskboxState: UseTasksPayload = {
tasks: mocks.tasks,
status: "idle",
error: null,
updateTaskState: fn()
};

// A super-simple mock of a redux store
const Mockstore = ({ taskboxState, children }: { taskboxState: State, children: ReactElement }) => (
<Provider
store={configureStore({
reducer: {
taskbox: createSlice({
name: "taskbox",
initialState: taskboxState,
reducers: {
updateTaskState: (state, action) => {
const { id, newTaskState } = action.payload;
const task = state.tasks.findIndex((task) => task.id === id);
if (task >= 0) {
state.tasks[task].state = newTaskState;
}
},
},
}).reducer,
},
})}
>
{children}
</Provider>
);

const meta = {
component: TaskList,
title: "TaskList",
Expand All @@ -52,85 +23,44 @@ export default meta;
type Story = StoryObj<typeof meta>

export const Default: Story = {
decorators: [
(story) => <Mockstore taskboxState={defaultTaskboxState}>{story()}</Mockstore>,
],
args: {
...defaultTaskboxState,
}
};

export const WithPinnedTasks: Story = {
decorators: [
(story) => {
const pinnedtasks: Task[] = [
...defaultTaskboxState.tasks.slice(0, 5),
{ id: "6", title: "Task 6 (pinned)", state: "TASK_PINNED" },
];

return (
<Mockstore
taskboxState={{
...defaultTaskboxState,
tasks: pinnedtasks,
}}
>
{story()}
</Mockstore>
);
},
],
args: {
...Default.args,
tasks: [
...defaultTaskboxState.tasks.slice(0, 5),
{ id: "6", title: "Task 6 (pinned)", state: "TASK_PINNED" },
],
}
};

export const Loading: Story = {
decorators: [
(story) => (
<Mockstore
taskboxState={{
...defaultTaskboxState,
status: "loading",
}}
>
{story()}
</Mockstore>
),
],
args: {
...Default.args,
status: "loading",
}
};

export const Empty: Story = {
decorators: [
(story) => (
<Mockstore
taskboxState={{
...defaultTaskboxState,
tasks: [],
}}
>
{story()}
</Mockstore>
),
],
args: {
...Default.args,
tasks: [],
}
};

export const TestPinBehavior: Story = {
...Default,
play: async ({ canvas, step }) => {

await step('Ensure tasks are rendered in the initial order', async () => {
const listItems = canvas.getAllByRole("listitem");
await expect(listItems[0]).toHaveTextContent("Learn more about Storybook");
await expect(listItems[1]).toHaveTextContent("Go to the gym");
})

await step('Pin "Go to the gym" task', async () => {
// Pin Learn more about Storybook and verify it moves to the top
const pinButton = canvas.getByLabelText("Pin Go to the gym");
await userEvent.click(pinButton);
});
play: async({ canvas, args }) => {
await userEvent.click(canvas.getByLabelText('Pin Learn more about Storybook'))
await expect(args.updateTaskState).toHaveBeenCalledWith(mocks.task.id, 'TASK_PINNED')

await step('Ensure tasks order is changed', async () => {
const updatedListItems = canvas.getAllByRole("listitem");
await expect(updatedListItems[0]).toHaveTextContent("Go to the gym");
await expect(updatedListItems[1]).toHaveTextContent("Learn more about Storybook");
});
await userEvent.click(canvas.getByLabelText('Archive Learn more about Storybook'))
await expect(args.updateTaskState).toHaveBeenCalledWith(mocks.task.id, 'TASK_ARCHIVED')
},
// hide the story from autodocs page as it's intended for test purposes only
tags: ['!autodocs']
};
};
33 changes: 14 additions & 19 deletions src/components/TaskList.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { useDispatch, useSelector } from "../lib/hooks";
import { updateTaskState } from "../lib/store";
import { selectFilteredTasks, selectTaskbox } from "../lib/selectors";
import Task from "./Task";
import { UseTasksPayload } from "#lib/useTasks.ts";
import TaskComponent from "./Task";

export default function TaskList() {
// We're retrieving our state from the store
const tasks = useSelector(selectFilteredTasks);
type TaskListProps = UseTasksPayload;

const { status } = useSelector(selectTaskbox);
export default function TaskList({ tasks = [], updateTaskState, status }: TaskListProps) {
// Filtered tasks (similar to the previous `selectFilteredTasks`)
const filteredTasks = [
...tasks.filter((t) => t.state === "TASK_PINNED"),
...tasks.filter((t) => t.state !== "TASK_PINNED"),
].filter((t) => t.state === "TASK_INBOX" || t.state === "TASK_PINNED");

const dispatch = useDispatch();
const pinTask = (id: string) => updateTaskState(id, "TASK_PINNED");

const pinTask = (id: string) => {
// We're dispatching the Pinned event back to our store
dispatch(updateTaskState({ id, newTaskState: "TASK_PINNED" }));
};
const archiveTask = (id: string) => {
// We're dispatching the Archive event back to our store
dispatch(updateTaskState({ id, newTaskState: "TASK_ARCHIVED" }));
};
const archiveTask = (id: string) => updateTaskState(id, "TASK_ARCHIVED");

if (status === "loading") {
return (
Expand Down Expand Up @@ -46,10 +40,11 @@ export default function TaskList() {
</div>
);
}

return (
<ul className="list-items" data-testid="success">
{tasks.map((task) => (
<Task
{filteredTasks.map((task) => (
<TaskComponent
key={task.id}
task={task}
onPinTask={(task) => pinTask(task)}
Expand Down
16 changes: 0 additions & 16 deletions src/lib/hooks.ts

This file was deleted.

21 changes: 0 additions & 21 deletions src/lib/selectors.ts

This file was deleted.

Loading

0 comments on commit 4d403e4

Please sign in to comment.