Skip to content
This repository was archived by the owner on Dec 15, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.21.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"web-vitals": "^2.1.0"
},
Expand All @@ -16,6 +18,7 @@
"@types/node": "^16.9.1",
"@types/react": "^17.0.20",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"typescript": "^4.4.3"
},
"scripts": {
Expand Down
39 changes: 39 additions & 0 deletions src/components/Users/User.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useCallback, useContext, useMemo } from 'react';
import { useHistory, useParams } from 'react-router-dom';

import { UsersContext } from 'context/Users';

import type { VFC } from 'react';

export const User: VFC<{}> = () => {
const history = useHistory();
const { users, error, loading, dispatch } = useContext(UsersContext);
const { userId } = useParams<{ userId?: string }>();

/** Finds the user for the current page; undefined if a user enters an invalid user id. */
const user = useMemo(() => users.find(u => u.id === Number(userId)), [users, userId]);

const deleteUser = useCallback(() => {
if (!user) return;

dispatch({ type: 'DELETE', payload: user });
history.push('/', { deletedUser: user }); // Back to the users list with the user we just deleted.
}, [history, dispatch, user]);

if (loading) return <h3>Loading…</h3>;
if (!user) return <h3>User {userId} not found.</h3>

return (
<div>
{error && <h4>Error: {error}</h4>}

<ul>
<li>id: {user.id}</li>
<li>name: {user.name}</li>
<li>email: {user.email}</li>
</ul>

<button onClick={deleteUser}>Delete</button>
</div>
);
}
51 changes: 51 additions & 0 deletions src/components/Users/Users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useCallback, useContext, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';

import { UsersContext } from 'context/Users';
import type { User } from 'context/Users';

import type { VFC } from 'react';

export const Users: VFC<{}> = () => {
const location = useLocation<{ deletedUser: User }>();
const { users, loading, error, dispatch } = useContext(UsersContext);

const showRestore = useMemo(() => {
if (!location.state?.deletedUser) return false;
// If the user still exists in the array (eg. we just restored it, don't show it)
// There's a far more performant way of doing this (during the restore action), but easy workaround with a very small array of users.
if (users.some(user => user.id === location.state.deletedUser.id)) return false;

return true;
}, [users, location.state?.deletedUser]);

const restoreUser = useCallback(() => {
if (!showRestore) return;

dispatch({
type: 'ADD',
payload: location.state.deletedUser,
});
}, [showRestore, dispatch, location.state?.deletedUser])

if (loading) return <h3>Loading…</h3>;

return <div>
{showRestore && (
<>
<h4>Deleted user {location.state.deletedUser.name} ({location.state.deletedUser.id})!</h4>
<button onClick={restoreUser}>Restore User {location.state.deletedUser.id}</button>
</>
)}

{error && <h4>Error: {error}</h4>}

<ul>
{users.map(user => (
<li key={user.id}>
<Link to={`/${user.id}`}>{user.id}: {user.name}</Link>
</li>
))}
</ul>
</div>;
}
2 changes: 2 additions & 0 deletions src/components/Users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './User';
export * from './Users';
38 changes: 0 additions & 38 deletions src/containers/App/App.css

This file was deleted.

8 changes: 0 additions & 8 deletions src/containers/App/App.test.tsx

This file was deleted.

25 changes: 0 additions & 25 deletions src/containers/App/App.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/containers/App/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/containers/App/logo.svg

This file was deleted.

42 changes: 42 additions & 0 deletions src/containers/Users/Router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BrowserRouter, Link, Route, Switch } from 'react-router-dom';

import { Users, User } from 'components/Users';

import { UsersProvider, UsersContext } from 'context/Users';

import type { ReactNode, VFC } from 'react';
import type { Context } from 'context/Users';

export const UsersRouter: VFC<{ children?: ReactNode }> = ({ children }) => (
<UsersProvider>
<BrowserRouter basename="/users">
<ul>
<li><Link to="/">Users</Link></li>
</ul>

{children}

<UsersContext.Consumer>
{({ users, loading, error, refresh }: Context) => (
<>
<h3>Users: {users.length}</h3>

<button onClick={() => refresh()}>Refresh Users</button>

{loading && <h4>Loading…</h4>}
{error && <h4>error: {error}</h4>}

<Switch>
<Route path="/:userId">
<User />
</Route>
<Route path="/">
<Users />
</Route>
</Switch>
</>
)}
</UsersContext.Consumer>
</BrowserRouter>
</UsersProvider>
)
1 change: 1 addition & 0 deletions src/containers/Users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Router';
45 changes: 45 additions & 0 deletions src/context/Users/Provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import axios from 'axios';

import { UsersContext, defaultUsersContext } from './context';
import { usersReducer } from './reducer';

import type { ReactNode, VFC } from 'react';

export const UsersProvider: VFC<{ children: ReactNode }> = ({ children }) => {
const [error, setError] = useState(defaultUsersContext.error);
const [loading, setLoading] = useState(defaultUsersContext.loading);
const [users, dispatch] = useReducer(usersReducer, defaultUsersContext.users);

const initializeUsers = useCallback(async () => {
try {
const { data } = await axios('https://jsonplaceholder.typicode.com/users')

dispatch({ type: 'INIT', payload: data });
setError(undefined);
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
}, [dispatch, setLoading, setError]);

/** Load all users initially. */
useEffect(() => {
initializeUsers();
}, [initializeUsers]);

const contextValue = useMemo(() => ({
users,
error,
dispatch,
loading,
refresh: initializeUsers,
}), [users, error, dispatch, loading, initializeUsers])

return (
<UsersContext.Provider value={contextValue}>
{children}
</UsersContext.Provider>
);
};
16 changes: 16 additions & 0 deletions src/context/Users/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createContext } from 'react';
import { Context, Action } from './types'

export const defaultUsersContext = {
loading: true,
error: undefined,
users: [],
dispatch: (action: Action) => {
throw new Error(`WARNING: UsersContext.dispatch attempted to fire with type=${action.type} before initialized by a provider.`)
},
refresh: () => {
throw new Error(`WARNING: UsersContext.refresh was called before initialized by a provider.`);
},
};

export const UsersContext = createContext<Context>(defaultUsersContext);
3 changes: 3 additions & 0 deletions src/context/Users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './context';
export * from './Provider';
export * from './types';
44 changes: 44 additions & 0 deletions src/context/Users/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from 'axios';

import { AddAction, DeleteAction, UpdateAction, InitAction, User, Action } from './types'

export const usersReducer = (users: User[] | undefined = [], { type, payload } : Action): User[] => {
switch (type) {
// Append a User.
case 'ADD':
const createUser = (payload as AddAction['payload']);

// NOTE: You'd probably await this or have multiple dispatches before/after async to temporarily and finally update.
axios.post(`https://jsonplaceholder.typicode.com/users`, createUser);

return [...users, createUser];

// Delete the User from the state.
case 'DELETE':
const deleteUser = (payload as DeleteAction['payload']);

// NOTE: You'd probably await this or have multiple dispatches before/after async to temporarily and finally update.
axios.delete(`https://jsonplaceholder.typicode.com/users/${deleteUser.id}`);

return users.filter(user => user.id !== deleteUser.id);

// Update/Replace a User; this returns the payload in its place, does not merge.
case 'UPDATE':
const updateUser = (payload as UpdateAction['payload']);

// NOTE: You'd probably await this or have multiple dispatches before/after async to temporarily and finally update.
axios.put(`https://jsonplaceholder.typicode.com/users/${updateUser.id}`, updateUser);

return users.map(user => {
if (user.id === updateUser.id) return updateUser;
return user;
});

case 'INIT':
// Override the entire Users state, eg. on initialization
return (payload as InitAction['payload']);

default:
throw new Error(`Unknown action passed to UsersReducer: ${type}.`)
}
}
33 changes: 33 additions & 0 deletions src/context/Users/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Dispatch } from 'react';

export interface User {
id: number;
name: string;
email: string;
}

export type AddAction = { type: 'ADD'; payload: User };
export type InitAction = { type: 'INIT'; payload: User[] };
export type DeleteAction = { type: 'DELETE'; payload: User };
export type UpdateAction = { type: 'UPDATE'; payload: User };

export type Action = AddAction | InitAction | DeleteAction | UpdateAction;

export interface Context {
/** NOTE: Error is not actually hooked up… */
error?: string;

/** Users will be an empty array when `loading=true`. */
loading: boolean;

/** The users state. */
users: User[];

/** Dispatch an action to the users */
dispatch: Dispatch<Action>;

/** Refresh the users state from API in the background.
* NOTE: Does not set `loading`.
*/
refresh: () => void;
}
4 changes: 2 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { StrictMode } from 'react';
import { render } from 'react-dom';
import './global.css';

import { App } from 'containers/App';
import { UsersRouter } from 'containers/Users';

render(
<StrictMode>
<App />
<UsersRouter />
</StrictMode>,
document.getElementById('root')
);
Loading