├── .env.example
├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.jsx
├── App.test.js
├── contexts
│ ├── auth.js
│ └── index.js
├── hooks
│ ├── index.js
│ ├── useAuth.js
│ ├── useProvideAuth.js
│ └── useUser.js
├── index.js
├── pages
│ ├── Home.jsx
│ ├── Login.jsx
│ ├── Profile.jsx
│ └── index.js
├── reportWebVitals.js
├── routes
│ ├── ProtectedRoute.jsx
│ └── index.jsx
└── setupTests.js
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BACKEND="http://localhost:8000"
--------------------------------------------------------------------------------
/.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
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 | .eslintcache
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-swr-auth
2 | A simple example of in memory token (JWT) implementation using React and useSWR library.
3 | This project is boostrapped with Create React App.
4 |
5 | ## Backend Used in This Project
6 | https://github.com/itsfaqih/jwt-php-native
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-router-dom": "^5.2.0",
12 | "react-scripts": "4.0.1",
13 | "swr": "^0.3.9",
14 | "web-vitals": "^0.2.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itsfaqih/react-swr-auth/f51c519351fd19fd7f687dbaf6cb93d703c16a7b/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/itsfaqih/react-swr-auth/f51c519351fd19fd7f687dbaf6cb93d703c16a7b/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itsfaqih/react-swr-auth/f51c519351fd19fd7f687dbaf6cb93d703c16a7b/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.jsx:
--------------------------------------------------------------------------------
1 | import Routes from './routes';
2 |
3 | function App() {
4 | return ;
5 | }
6 |
7 | export default App;
8 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/contexts/auth.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const AuthContext = createContext();
4 |
5 | export default AuthContext;
6 |
--------------------------------------------------------------------------------
/src/contexts/index.js:
--------------------------------------------------------------------------------
1 | import AuthContext from './auth';
2 |
3 | export { AuthContext };
4 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import useAuth from './useAuth';
2 | import useProvideAuth from './useProvideAuth';
3 | import useUser from './useUser';
4 |
5 | export { useAuth, useProvideAuth, useUser };
6 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { AuthContext } from '../contexts';
3 |
4 | export default function useAuth() {
5 | return useContext(AuthContext);
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useProvideAuth.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import useSWR from 'swr';
3 |
4 | export default function useProvideAuth() {
5 | const [token, setToken] = useState(null);
6 | const [isLogin, setIsLogin] = useState(!!JSON.parse(localStorage.getItem('isLogin')));
7 | const [isLoading, setIsLoading] = useState(true);
8 |
9 | const login = token => {
10 | localStorage.setItem('isLogin', true);
11 | setToken(token);
12 | setIsLogin(true);
13 | };
14 |
15 | const logout = () => {
16 | localStorage.removeItem('isLogin');
17 | setToken(null);
18 | setIsLogin(false);
19 | };
20 |
21 | // Refresh token for persisting session
22 | const { data, error, isValidating } = useSWR(
23 | isLogin ? `${process.env.REACT_APP_BACKEND}/refresh-token.php` : null,
24 | url =>
25 | fetch(url, {
26 | credentials: 'include',
27 | }).then(res => res.json()),
28 | {
29 | // Silently refresh token every expiry time
30 | refreshInterval: 1000 * 60 * 15,
31 | revalidateOnFocus: false
32 | }
33 | );
34 |
35 | useEffect(() => {
36 | if (data) {
37 | login(data.accessToken);
38 | }
39 | if (error) {
40 | logout();
41 | }
42 | setIsLoading(isValidating);
43 | }, [data, error, isValidating]);
44 |
45 | useEffect(() => {
46 | // Sync all tabs on login or logout
47 | window.addEventListener('storage', e => {
48 | if (e.key === 'isLogin') {
49 | setIsLogin(e.newValue);
50 | }
51 | });
52 | });
53 |
54 | return { token, login, logout, isLogin, isLoading };
55 | }
56 |
--------------------------------------------------------------------------------
/src/hooks/useUser.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import useSWR from 'swr';
3 | import useAuth from './useAuth';
4 |
5 | export default function useUser() {
6 | const { token } = useAuth();
7 | const [user, setUser] = useState(null);
8 |
9 | // Get user data
10 | const { data, error, mutate } = useSWR(
11 | token ? `${process.env.REACT_APP_BACKEND}/me.php` : null,
12 | url =>
13 | fetch(url, {
14 | method: 'GET',
15 | headers: {
16 | 'Authorization': `Bearer ${token}`,
17 | },
18 | }).then(res => res.json()),
19 | { revalidateOnFocus: false }
20 | );
21 |
22 | useEffect(() => {
23 | if (data) {
24 | setUser(data);
25 | }
26 | if (error) {
27 | setUser(null);
28 | }
29 | }, [data, error]);
30 |
31 | return { user, mutate };
32 | }
33 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import reportWebVitals from './reportWebVitals';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
13 | // If you want to start measuring performance in your app, pass a function
14 | // to log results (for example: reportWebVitals(console.log))
15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
16 | reportWebVitals();
17 |
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useAuth } from '../hooks';
3 |
4 | export default function Home() {
5 | const auth = useAuth();
6 |
7 | return (
8 |
9 |
Welcome!
10 | {auth.isLogin ? (
11 |
12 | -
13 | Profile
14 |
15 | -
16 |
17 |
18 |
19 | ) : (
20 |
Login
21 | )}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useAuth } from '../hooks';
4 |
5 | export default function Login() {
6 | const auth = useAuth();
7 | const [data, setData] = useState(null);
8 | const [isLoading, setIsLoading] = useState(false);
9 | const [error, setError] = useState(null);
10 |
11 | const handleChange = e => {
12 | const { name, value } = e.target;
13 | setData(data => ({ ...data, [name]: value }));
14 | };
15 |
16 | const handleSubmit = e => {
17 | e.preventDefault();
18 | setIsLoading(true);
19 |
20 | fetch(`${process.env.REACT_APP_BACKEND}/login.php`, {
21 | method: 'POST',
22 | credentials: 'include',
23 | body: JSON.stringify(data),
24 | })
25 | .then(res => res.json())
26 | .then(json => {
27 | if (json.success) {
28 | auth.login(json.data.token);
29 | } else {
30 | setError(json.message);
31 | setIsLoading(false);
32 | }
33 | });
34 | };
35 |
36 | return (
37 |
38 |
53 | {error &&
{error}
}
54 |
Back to home
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useUser } from '../hooks';
3 |
4 | export default function Profile() {
5 | const { user } = useUser();
6 |
7 | if (!user) return Loading...
;
8 |
9 | return (
10 |
11 |
Name: {user.name}
12 |
Email: {user.email}
13 |
Back to home
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import Home from './Home';
2 | import Login from './Login';
3 | import Profile from './Profile';
4 |
5 | export { Home, Login, Profile };
6 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/routes/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Redirect } from 'react-router-dom';
2 | import { useAuth } from '../hooks';
3 |
4 | export default function ProtectedRoute({ component: Component, reverse, ...props }) {
5 | const auth = useAuth();
6 |
7 | if (auth.isLoading) {
8 | return Loading...
;
9 | }
10 | if (!auth.isLogin) {
11 | if (reverse === true) {
12 | return ;
13 | }
14 | return ;
15 | }
16 | if (reverse === true) {
17 | return ;
18 | }
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/routes/index.jsx:
--------------------------------------------------------------------------------
1 | import { Route, BrowserRouter as Router, Switch } from 'react-router-dom';
2 | import { useProvideAuth } from '../hooks';
3 | import { AuthContext } from '../contexts';
4 | import { Home, Login, Profile } from '../pages';
5 | import ProtectedRoute from './ProtectedRoute';
6 |
7 | export default function Routes() {
8 | const auth = useProvideAuth();
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------