Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Redux with React Context #121

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
48 changes: 34 additions & 14 deletions src/app.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React from 'react';
import { Provider, useSelector } from 'react-redux';
import React, { useContext } from 'react';
import { Router } from 'react-router';
import { Switch, Route, Redirect } from 'react-router-dom';

import history from 'services/history.service';
import * as loaderService from 'services/loader.service';
import * as socketService from 'services/socket.service';
import api from 'services/api.service';

import store from 'resources/store';

import * as userSelectors from 'resources/user/user.selectors';
import { userActions } from 'resources/user/user.slice';
import ContextStoreProvider, { StoreContext } from 'resources/store/store';
import actions from 'resources/store/actions';

import Toast from 'components/toast';
import Loading from 'components/loading';
Expand All @@ -26,19 +24,33 @@ import Reset from 'pages/reset';
import Home from 'pages/home';
import NotFound from 'pages/not-found';

import 'resources/user/user.handlers';

import 'styles/main.pcss';

const Profile = React.lazy(() => import('./pages/profile'));

function PrivateScope({ children }) {
const user = useSelector(userSelectors.selectUser);
const { state, dispatch } = useContext(StoreContext);
const { user } = state;

React.useEffect(() => {
socketService.connect();

api.on('error', (error) => {
if (error.status === 401) {
dispatch(actions.signOut());
}
});

socketService.on('connect', () => {
socketService.emit('subscribe', `user-${user?._id}`);
});

socketService.on('user:updated', (updatedUser) => {
dispatch(actions.setUser({ user: updatedUser }));
});

return () => socketService.disconnect();
}, []);
}, [user?._id, dispatch]);

if (!user) {
const searchParams = new URLSearchParams({ to: window.location.pathname });
Expand Down Expand Up @@ -86,11 +98,12 @@ Object.values(scope).forEach((s, scopeIndex) => {

function App() {
const [loading, setLoading] = React.useState(true);
const { dispatch } = useContext(StoreContext);

React.useEffect(() => {
async function init() {
try {
await store.dispatch(userActions.getCurrentUser());
await dispatch(actions.getCurrentUser());
} catch (error) {
// @todo: add something like sentry
} finally {
Expand All @@ -100,12 +113,13 @@ function App() {
}

init();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (loading) return null;

return (
<Provider store={store}>
<>
<Router history={history}>
<ErrorBoundary fallback={<h1>Error!</h1>}>
<React.Suspense fallback={<Loading />}>
Expand Down Expand Up @@ -135,8 +149,14 @@ function App() {
</Router>

<Toast />
</Provider>
</>
);
}

export default App;
const Root = () => (
<ContextStoreProvider>
<App />
</ContextStoreProvider>
);

export default Root;
24 changes: 11 additions & 13 deletions src/components/toast/toast.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import React, { useContext } from 'react';
import cn from 'classnames';
import ReactDOM from 'react-dom';
import { useSelector, useDispatch } from 'react-redux';

import * as toastSelectors from 'resources/toast/toast.selectors';
import { toastActions } from 'resources/toast/toast.slice';
import { StoreContext } from 'resources/store/store';
import actions from 'resources/store/actions';

import Icon from 'components/icon';
import IconButton from 'components/icon-button';
Expand Down Expand Up @@ -39,9 +38,8 @@ function getIconProps(type) {
}

function RawToast() {
const dispatch = useDispatch();

const messages = useSelector(toastSelectors.selectMessages);
const { state, dispatch } = useContext(StoreContext);
const { toast: messages } = state;

const element = React.useRef(document.createElement('div'));

Expand All @@ -57,24 +55,24 @@ function RawToast() {
function list() {
return (
<>
{messages.map((m) => {
{messages.map(({ id, message, type }) => {
const closeToast = () => {
dispatch(toastActions.remove({ id: m.id }));
dispatch(actions.removeToast(id));
};

const iconProps = getIconProps(m.type);
const iconProps = getIconProps(type);
return (
<div
key={m.id}
className={cn(styles.toast, styles[m.type])}
key={id}
className={cn(styles.toast, styles[type])}
>
<div className={styles.main}>
<Icon
icon={iconProps.icon}
color={iconProps.color}
noWrapper
/>
<span>{m.text}</span>
<span>{message}</span>
</div>
<IconButton
icon="close"
Expand Down
22 changes: 11 additions & 11 deletions src/components/toast/toast.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
import React from 'react';
import { Provider, useDispatch } from 'react-redux';
import store from 'resources/store';
import React, { useContext } from 'react';

import ContextStoreProvider, { StoreContext } from 'resources/store/store';
import actions from 'resources/store/actions';

import { toastActions } from 'resources/toast/toast.slice';
import Button from 'components/button';

export default {
title: 'Components/Toaster',
component: Button,
decorators: [
(Story) => (
<Provider store={store}>
<ContextStoreProvider>
<Story />
</Provider>
</ContextStoreProvider>
),
],
};

export const Template = () => {
const dispatch = useDispatch();
const { dispatch } = useContext(StoreContext);

const showSuccessToast = () => {
dispatch(toastActions.success('This is success toast! This is success toast!'));
dispatch(actions.createToast({ type: 'success', message: 'This is success toast! This is success toast!' }));
};

const showErrorToast = () => {
dispatch(toastActions.error('This is error toast! This is error toast!'));
dispatch(actions.createToast({ type: 'error', message: 'This is error toast! This is error toast!' }));
};

const showInfoToast = () => {
dispatch(toastActions.info('This is info toast! This is info toast!'));
dispatch(actions.createToast({ type: 'info', message: 'This is info toast! This is info toast!' }));
};

const showWarningToast = () => {
dispatch(toastActions.warning('This is warning toast! This is warning toast!'));
dispatch(actions.createToast({ type: 'warning', message: 'This is warning toast! This is warning toast!' }));
};

return (
Expand Down
10 changes: 5 additions & 5 deletions src/layouts/main/header/components/user-menu/user-menu.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import React, { useContext } from 'react';
import cn from 'classnames';
import { Link, useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';

import { routes } from 'routes';

import { userActions } from 'resources/user/user.slice';
import { StoreContext } from 'resources/store/store';
import actions from 'resources/store/actions';

import styles from './user-menu.styles.pcss';

Expand All @@ -17,8 +17,8 @@ const linksList = [
];

function UserMenu() {
const dispatch = useDispatch();
const history = useHistory();
const { dispatch } = useContext(StoreContext);

const [isOpen, setIsOpen] = React.useState(false);
function toggleIsOpen() {
Expand All @@ -36,7 +36,7 @@ function UserMenu() {
}, []);

async function logout() {
await dispatch(userActions.signOut());
await dispatch(actions.signOut());
history.push(routes.signIn.path);
}

Expand Down
19 changes: 9 additions & 10 deletions src/pages/forgot/forgot.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import React, { useContext } from 'react';
import cn from 'classnames';
import { Link, Redirect } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';

import { routes } from 'routes';

import * as userSelectors from 'resources/user/user.selectors';
import { userActions } from 'resources/user/user.slice';
import { StoreContext } from 'resources/store/store';

import * as api from 'resources/user/user.api';

import Input from 'components/input';
import Button from 'components/button';
Expand All @@ -15,18 +15,17 @@ import Form from 'components/form';
import styles from './forgot.pcss';

const Forgot = () => {
const dispatch = useDispatch();

const user = useSelector(userSelectors.selectUser);
const { state } = useContext(StoreContext);
const { user } = state;

const [values, setValues] = React.useState({});
const [pending, setPending] = React.useState(false);
const [submitted, setSubmitted] = React.useState(false);

const handleSubmit = async (submitValues) => {
const handleSubmit = async ({ email }) => {
setPending(true);
await dispatch(userActions.forgot(submitValues));
setValues(submitValues);
await api.forgot({ email });
setValues({ email });
setSubmitted(true);
setPending(false);
};
Expand Down
32 changes: 16 additions & 16 deletions src/pages/profile/profile.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useCallback, useState, useEffect } from 'react';
import React, {
useCallback, useState, useEffect, useContext,
} from 'react';
import * as yup from 'yup';
import { useDispatch, useSelector } from 'react-redux';
import { useFormContext } from 'react-hook-form';

import * as userSelectors from 'resources/user/user.selectors';
import { userActions } from 'resources/user/user.slice';
import { toastActions } from 'resources/toast/toast.slice';
import { StoreContext } from 'resources/store/store';
import actions from 'resources/store/actions';

import * as filesApi from 'resources/files/files.api';

import Input from 'components/input';
Expand All @@ -30,9 +31,8 @@ const schema = yup.object({
.email('Please enter a valid email address'),
});

const CancelButton = ({ onCancel }) => { // eslint-disable-line react/prop-types
const CancelButton = ({ onCancel, user }) => { // eslint-disable-line react/prop-types
const { reset } = useFormContext();
const user = useSelector(userSelectors.selectUser);

const handleClick = () => {
onCancel();
Expand All @@ -52,16 +52,15 @@ const CancelButton = ({ onCancel }) => { // eslint-disable-line react/prop-types
};

const Profile = () => {
const dispatch = useDispatch();
const user = useSelector(userSelectors.selectUser);
const { avatarFileKey: userAvatarFileKey } = user;
const { state: { user }, dispatch } = useContext(StoreContext);
const { avatarFileKey: userAvatarFileKey } = user || {};

const [avatarFileKey, setAvatarFileKey] = useState(userAvatarFileKey);
const [avatarUrl, setAvatarUrl] = useState(null);

const handleSubmit = useCallback(async (submitValues) => {
await dispatch(userActions.updateCurrentUser({ ...submitValues, avatarFileKey }));
dispatch(toastActions.success('User info updated!'));
await dispatch(actions.updateCurrentUser({ ...submitValues, avatarFileKey }));
dispatch(actions.createToast({ type: 'success', message: 'User info updated!' }));
}, [dispatch, avatarFileKey]);

const getAvatarUrl = useCallback(async (key) => {
Expand All @@ -70,8 +69,8 @@ const Profile = () => {
if (url) {
setAvatarUrl(url);
}
} catch (error) {
dispatch(toastActions.error(error));
} catch ({ data }) {
dispatch(actions.createToast({ type: 'error', message: data.errors.file[0] }));
}
}, [dispatch]);

Expand All @@ -80,8 +79,8 @@ const Profile = () => {
const file = files[0];
const { key } = await filesApi.upload(file);
setAvatarFileKey(key);
} catch (error) {
dispatch(toastActions.error(error));
} catch ({ data }) {
dispatch(actions.createToast({ type: 'error', message: data.errors.file[0] }));
}
};

Expand Down Expand Up @@ -162,6 +161,7 @@ const Profile = () => {
<div className={styles.buttons}>
<CancelButton
onCancel={resetAvatar}
user={user}
/>
<Button
className={styles.button}
Expand Down
Loading