React PWA + Feathers API
Today we will extend on one of the pervious apps we've worked before. We used react hooks to create this Todo application That app used localStorage to save the state. We will transform it to a full blown PWA with help of Feathers.js. If you want to brush up on your skills on React Hooks or ...
Today we will extend on one of the pervious apps we've worked before. We used react hooks to create this Todo application
That app used localStorage to save the state. We will transform it to a full blown PWA with help of Feathers.js.
If you want to brush up on your skills on React Hooks or Feathers.js Please feel free to refer to these previous articles.
Intro to Feathers.js
Feathers hooks
Todo using React Hooks
Because we've generated our app using create-react-app, we don't have to do a lot to turn our app offline capable. create-react-app by default comes with all the settings in place conveniently. But, for development purpose, it's disabled by default.
To enable, navigate to src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister(); <<=============== We need to change this
Find the serviceWorker.unregister() (at the bottom) line and replace it with
serviceWorker.register()
Now, our app is primed to register the serviceWorker on pageload. But, there are some gotchas.
// This optional code is used to register a service worker. // register() is not called by default. // This lets the app load faster on subsequent visits in production, and gives // it offline capabilities. However, it also means that developers (and users) // will only see deployed updates on subsequent visits to a page, after all the // existing tabs open on the page have been closed, since previously cached // resources are updated in the background. // To learn more about the benefits of this model and instructions on how to // opt-in, read https://bit.ly/CRA-PWA const isLocalhost = Boolean( <<============ This will block sw from loading in development env. window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. window.location.hostname === '[::1]' || // 127.0.0.1/8 is considered localhost for IPv4. window.location.hostname.match( /^127(?:.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) ); export function register(config) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to // serve assets; see https://github.com/facebook/create-react-app/issues/2374 return; } window.addEventListener('load', () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { // This is running on localhost. Let's check if a service worker still exists or not. checkValidServiceWorker(swUrl, config); // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( 'This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://bit.ly/CRA-PWA' ); }); } else { // Is not localhost. Just register service worker registerValidSW(swUrl, config); } }); } } function registerValidSW(swUrl, config) { navigator.serviceWorker .register(swUrl) .then(registration => { registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { return; } installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( 'New content is available and will be used when all ' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' ); // Execute callback if (config && config.onUpdate) { config.onUpdate(registration); } } else { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. console.log('Content is cached for offline use.'); // Execute callback if (config && config.onSuccess) { config.onSuccess(registration); } } } }; }; }) .catch(error => { console.error('Error during service worker registration:', error); }); } function checkValidServiceWorker(swUrl, config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl) .then(response => { // Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get('content-type'); if ( response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1) ) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => { window.location.reload(); }); }); } else { // Service worker found. Proceed as normal. registerValidSW(swUrl, config); } }) .catch(() => { console.log( 'No internet connection found. App is running in offline mode.' ); }); } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { registration.unregister(); }); } }
The way create-react-app designed it's service worker, in order for it to work, we have to use it as production environment. You can just modify isLocalhost to false, and continue working.
const isLocalhost = false;
But a much better way is, to run it from production env. For that run
npm i -g serve # if you don't have it installed already npm run build # generate the build folder serve -s build # serve !
Now to production server should be running at localhost:5000. Let's test.
Because we will use Feathers.js this is extremely easy part. Will take us less than 5 minutes to be up and running with the API.
So, let's create a new folder, and generate the app
mkdir feathers-todo feathers generate app
select the default options (we disabled authentication for the sake of bravety)
now, lets create the todo service
feathers generate service
Just doing this we will have all the necessary api for our app. For now, keep the feathers server running by running npm start.
Let's focus back on our React Todo and make it work with the feathers api.
We'll use axios for this. Run this and install axios on your code
npm i -s axios
Open App.js and import axios on top.
.... import axios from 'axios';
Now, let's see our old App.js
if (localStorage.getItem('todoStore') !== null) { store = JSON.parse(localStorage.getItem('todoStore')); } else { store = [{ item: "example todo", isCompleted: false }] }
We were checking if our browser's localStorage had previous todos stored, and reading that as our store object.
Let's change that to read from our feathers api
axios.get('localhost:3030/todos') // 3030 is feathers app running .then(function (response) { setTodos(response.data.data); }) .catch(function (error) { console.log(error); });
Fire up the react server and let's run.
We are blocked by the CORS security countermeasures. No worries, it's just a one line change to make it work. Open the package.json file, and add the "proxy": "http://localhost:3030" line at the bottom.
{ "name": "react-todo", "version": "0.1.0", "private": true, "dependencies": { "axios": "^0.19.0", "font-awesome": "^4.7.0", "react": "^16.9.0", "react-dom": "^16.9.0", "react-scripts": "3.1.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "proxy": "http://localhost:3030" <<============ Add this }
This will tell react app to redirect any path it can't recognize to the proxy path. Easy as that. That being done. let's modify our code, we can just call axios.get('/todos') now, no need to define full path.
axios.get('/todos') .then(function (response) { setTodos(response.data.data); }) .catch(function (error) { console.log(error); });
Create
Time to modify our create todo method. The previous method only saved data to localStorage.
const addItem = text => { const newTodos = [...todos, { item: text }]; setTodos(newTodos); localStorage.setItem('todoStore', JSON.stringify(newTodos)); }
Let's change it to actually do api call. After we get response from the server, we'll call the setTodos(todo) method and update the UI.
const addItem = text => { const todo = { item: text, isCompleted: false }; const newTodos = [...todos, todo ]; axios.post('/todos', todo) .then(function (response) { setTodos(newTodos); }) .catch(function (error) { console.log(error); }); }
Update
const complete = index => { const newTodos = [...todos]; const todo = newTodos[index]; todo.isCompleted = !todo.isCompleted setTodos(newTodos); axios.put(`/todos/${todo._id}`, todo) .then(function (response) { setTodos(newTodos); }) .catch(function (error) { console.log(error); }); }
Remove
const removeItem = index => { const newTodos = [...todos]; const removedTodo = newTodos.splice(index, 1); axios.delete(`/todos/${removedTodo[0]._id}`) .then(function (response) { setTodos(newTodos); }) .catch(function (error) { console.log(error); }); }
And we're done !
React App
Feathers App