{"componentChunkName":"component---src-templates-page-tsx","path":"/","result":{"pageContext":{"searchData":[{"id":"cjjg9ui0t303s010375m42b9w","title":"Turn a Browser Window into a Notepad with This One-Liner","slug":"/turn-a-browser-window-into-a-notepad-with-this-one-liner","avatar":"https://s3.amazonaws.com/contentkit/static/cjjg9ui0t303s010375m42b9w/OYIaJ1KK_400x400(1).png","date":"July 1, 2020"},{"id":"cjiy7xl9o17w701039gvl18a5","title":"Creating a Collaborative Editor with Draftjs for Fun","slug":"/draft-js-collaborative-editor","avatar":"https://s3.amazonaws.com/contentkit/static/cjiy7xl9o17w701039gvl18a5/draft-js.png","date":"June 28, 2020"},{"id":"cjeqgfsdsnmx90167n7j290kt","title":"How To Include SASS In Your React Project","slug":"/sass-react-webpack","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfsdsnmx90167n7j290kt/parcel-bundler.png","date":"May 1, 2020"},{"id":"cjiy4tseq0tnw0103bc5s7sxj","title":"How to Add a Loading Indicator to Material Ui's Component","slug":"/creating-a-material-ui-button-with-spinner-that-reflects-loading-state","avatar":"https://s3.amazonaws.com/contentkit/static/cjiy4tseq0tnw0103bc5s7sxj/material-ui.png","date":"January 28, 2020"},{"id":"cjkq4u7470ihr0157ad175b12","title":"Connecting to AWS Lambda via WebSockets","slug":"/connecting-to-aws-lambda-via-websockets","avatar":"https://s3.amazonaws.com/contentkit/static/cjkq4u7470ihr0157ad175b12/MQTT.js.png","date":"January 12, 2020"},{"id":"cjeqgfnhknnpe0199zbfwwkd8","title":"6 Really Cool APIs to Have Fun With","slug":"/cool-apis-to-have-fun-with","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfnhknnpe0199zbfwwkd8/scale-api.png","date":"January 1, 2020"},{"id":"cjeqgftvknnr30199dvw266qh","title":"Installing Node Canvas in AWS Lambda","slug":"/node-canvas-aws-lambda","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgftvknnr30199dvw266qh/aws-lambda.jpg","date":"December 31, 2019"},{"id":"cjn5fv8kl0pfj0124wwebufms","title":"GoLang Cheatsheet","slug":"/golang-cheatsheet","avatar":"https://s3.amazonaws.com/contentkit/static/cjn5fv8kl0pfj0124wwebufms/golang.png","date":"October 13, 2019"},{"id":"cjn15y8740liw0175u3s707eh","title":"Scheduled Jobs & Work Queues With Postgresql","slug":"/scheduled-jobs-work-queues-with-postgresql","avatar":"https://s3.amazonaws.com/contentkit/static/cjn15y8740liw0175u3s707eh/kxHkAenZ_400x400.jpg","date":"October 8, 2019"},{"id":"cjmym86fh0jy1013398nfzuqu","title":"Creating a Bot to Refill Parking Meters Using AWS Lambda","slug":"/creating-a-bot-to-refill-parking-meters-using-aws-lambda","avatar":"https://s3.amazonaws.com/contentkit/static/cjmym86fh0jy1013398nfzuqu/prwHMlRn_400x400.jpg","date":"October 7, 2019"},{"id":"cjmsaqt6l09le01046qeukpp9","title":"The USPS Tracking API: How To Track Packages","slug":"/usps-tracking-api","avatar":"https://s3.amazonaws.com/contentkit/static/cjmsaqt6l09le01046qeukpp9/usps.png","date":"October 3, 2019"},{"id":"cjeqgfuyenmy20167rycnmiql","title":"Stormpath vs Firebase - A Side-By-Side Comparison","slug":"/stormpath-vs-firebase-a-side-by-side-comparison","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfuyenmy20167rycnmiql/m3cEA33V_400x400.jpg","date":"September 2, 2019"},{"id":"cjeqgfv88nnrt01997wxd4rnl","title":"Sending emails with Firebase Cloud Functions","slug":"/firebase-functions-sending-emails","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfv88nnrt01997wxd4rnl/firebase.jpg","date":"September 2, 2019"},{"id":"cjeqgfqjknnq20199oe3zwi37","title":"Deploying To DigitalOcean From Travis","slug":"/deploying-to-digitalocean-travis","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfqjknnq20199oe3zwi37/digitalocean.jpeg","date":"August 25, 2019"},{"id":"cjkvpbr6p06z50181fg7xvsc3","title":"Circumventing AWS Lambda's Bundle Size Limit","slug":"/circumventing-aws-lambdas-bundle-size-limit","avatar":"https://s3.amazonaws.com/contentkit/static/cjkvpbr6p06z50181fg7xvsc3/b2lWK7c0_400x400.png","date":"August 15, 2019"},{"id":"cjiy45h7m0iba0111f5imt5em","title":"A Quick Way to List All Unicode Characters (Javascript)","slug":"/unicode-characters-javascript","avatar":"https://s3.amazonaws.com/contentkit/static/cjiy45h7m0iba0111f5imt5em/unicode.png","date":"July 29, 2019"},{"id":"ci1rxc41tm8yi7nd156pz9dom","title":"Kibana Rest API","slug":"/kibana-rest-api","avatar":"https://s3.amazonaws.com/contentkit/static/ci1rxc41tm8yi7nd156pz9dom/elastic.png","date":"July 24, 2019"},{"id":"cjeqgfjgennmw0199wha3j6u0","title":"Airtable As A Database For Middleman [Tutorial]","slug":"/airtable-middleman-database","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfjgennmw0199wha3j6u0/airtable-books.png","date":"June 1, 2019"},{"id":"cjeqgfj5qnnmr0199vzd85xuo","title":"Building A Multiplayer Game With Three.Js + WebSockets","slug":"/multiplayer-game-threejs","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfj5qnnmr0199vzd85xuo/three.jpg","date":"June 1, 2019"},{"id":"cjeqgfi8hnnml0199f1jyo1p5","title":"The Best Sketch Plugins","slug":"/the-best-sketch-plugins","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfi8hnnml0199f1jyo1p5/sketch.png","date":"June 1, 2019"},{"id":"d9920hsu8y7365frf2c6fxrx2","title":"Configuring Vault by Hashicorp in AWS EC2","slug":"/configuring-vault-by-hashicorp-in-aws-ec2","avatar":"https://s3.amazonaws.com/contentkit/static/d9920hsu8y7365frf2c6fxrx2/vault.png","date":"April 15, 2019"},{"id":"cjeqgfvsfnns00199mlbff48i","title":"Best Zapier Alternatives","slug":"/best-zapier-alternatives","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfvsfnns00199mlbff48i/zapier-alternative-workato.png","date":"January 31, 2019"},{"id":"cjn3l18390lyt0158i8qxs5dc","title":"Running A Simple Node Web Server On AWS EC2","slug":"/running-a-simple-node-web-server-on-aws-ec2","avatar":"https://s3.amazonaws.com/contentkit/static/cjn3l18390lyt0158i8qxs5dc/aws.png","date":"October 11, 2018"},{"id":"cjeqgfr4snmwz01673m8l4i51","title":"Graph.Cool vs Firebase","slug":"/graphcool-vs-firebase","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfr4snmwz01673m8l4i51/graphcool.png","date":"October 7, 2018"},{"id":"cjklurup00k0201739mflg9sz","title":"Accessing Redis on an Aws EC2 Instance from the Outside\t","slug":"/accessing-redis-on-an-aws-ec2-instance-from-the-outside","avatar":"https://s3.amazonaws.com/contentkit/static/cjklurup00k0201739mflg9sz/logo.jpeg","date":"August 9, 2018"},{"id":"cjkfzvvxt08up0191l38aadq4","title":"Using PDFtk in AWS Lamba","slug":"/using-pdftk-in-aws-lamba","avatar":"https://s3.amazonaws.com/contentkit/static/cjkfzvvxt08up0191l38aadq4/pdftk.png","date":"August 4, 2018"},{"id":"cjeqgfqsgnnq70199l4vc6vim","title":"Configuring WebSockets on Elastic Beanstalk/EC2","slug":"/websockets-aws-elasticbeanstalk-ec2","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfqsgnnq70199l4vc6vim/aws.png","date":"July 15, 2018"},{"id":"cjit96q2yk0is0111qpbmw66s","title":"Getting Started With Gmail API","slug":"/gmail-api-quickstart","avatar":"https://s3.amazonaws.com/contentkit/static/cjit96q2yk0is0111qpbmw66s/gmail.png","date":"June 28, 2018"},{"id":"cjeqgfo23nmwd0167ynqe7xi8","title":"5 Tips For Using NextJs","slug":"/5-tips-for-using-nextjs","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfo23nmwd0167ynqe7xi8/bHjpwZem_400x400.png","date":"June 1, 2018"},{"id":"cjhxixcyhw4ze01035butoukz","title":"React unstable_deferredUpdates","slug":"/react-unstable_deferredupdates","avatar":"https://s3.amazonaws.com/contentkit/static/cjhxixcyhw4ze01035butoukz/OYIaJ1KK_400x400(1).png","date":"June 1, 2018"},{"id":"cjeqgflpnnnoy01993y7aez8a","title":"Simple Web Scraping With Javascript","slug":"/simple-web-scraping","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgflpnnnoy01993y7aez8a/developer-tools-scraping.png","date":"May 1, 2018"},{"id":"cjeqgfk9lnnn101994ll4ursr","title":"Easy: Add Firebase Facebook Login To Your React App","slug":"/firebase-facebook-login-react","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfk9lnnn101994ll4ursr/firebase.png","date":"May 1, 2018"},{"id":"cjeqgfiqrnmtj0167dmz5g5dg","title":"The 5 Best Static Site Web Hosts","slug":"/the-5-best-static-site-web-hosts","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfiqrnmtj0167dmz5g5dg/digital-ocean.png","date":"May 1, 2018"},{"id":"cjeqgfmi2nmvs01670pgebekn","title":"Tips and Tricks For Using NightmareJs","slug":"/nightmare-js","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfmi2nmvs01670pgebekn/nightmare.png","date":"May 1, 2018"},{"id":"cjeqgfkqennn60199707yp1fb","title":"Why You Should Create Your Next React Web App With Firebase","slug":"/firebase-react-tutorial","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfkqennn60199707yp1fb/firebase.jpg","date":"May 1, 2018"},{"id":"cjeqgfpr6nnpx019978qzmaok","title":"How to Flush Data From Heroku Redis","slug":"/heroku-redis-flushall","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfpr6nnpx019978qzmaok/3wgIDj3j_400x400.png","date":"April 1, 2018"},{"id":"cjeqgfm8xnnp30199vxndhkgh","title":"5 Ways To Style React Components","slug":"/5-ways-to-style-react-components","avatar":null,"date":"April 1, 2018"},{"id":"cjeqgflz7nmvm0167yo7wsjg3","title":"Delete Spreadsheet Rows For Google Sheets","slug":"/delete-rows-google-sheets","avatar":null,"date":"April 1, 2018"},{"id":"cjeqgfp84nnps0199dvru09ll","title":"Make An Uptime Monitoring Microservice In Under 50 Lines of Code","slug":"/twilio-uptime-monitoring-node-tutorial","avatar":null,"date":"April 1, 2018"},{"id":"cjeqgfri4nnqd0199zsv6a9fl","title":"Hexo - The Best Static Site Generator? ","slug":"/deploy-a-hexo-blog","avatar":null,"date":"April 1, 2018"},{"id":"cjeqgfoygnnpn0199f31dgk9m","title":"Airtable As A Minimum Viable Database For Your ReactJs Project","slug":"/airtable-reactjs","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfoygnnpn0199f31dgk9m/airtable-screenshot.png","date":"March 2, 2018"},{"id":"cjeqgfn6pnmw0016793f81hpx","title":"The Advanced Guide To ReactJs Checkboxes","slug":"/reactjs-checkboxes","avatar":null,"date":"January 20, 2018"},{"id":"cjeqgfe8znnly01991jo5xkxx","title":"Minimum Viable GraphQL QuickStart","slug":"/minimum-viable-graphql-quickstart","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfe8znnly01991jo5xkxx/graphcool-schema.png","date":"January 1, 2018"},{"id":"cjeqgfgegnnmb0199jp3hv2xx","title":"How To Add A Contact Form To Your Ghost Blog","slug":"/ghost-blog-contact-form","avatar":null,"date":"November 1, 2017"},{"id":"cjeqgfmvxnnp90199bdti4r8n","title":"PHP Scraping Tutorial - Scrape Reddit With Goutte","slug":"/php-scraping-tutorial-scrape-reddit-with-goutte","avatar":null,"date":"October 7, 2017"},{"id":"cjeqgfpz1nmwp0167c5j46eij","title":"Cannot read property 'loose' of undefined","slug":"/cannot-read-property-loose-of-undefined","avatar":null,"date":"August 25, 2017"},{"id":"cjeqgftlanmxg0167zipvfzp1","title":"Airtable API Example & Tutorial - Generating Charts","slug":"/airtable-api-example-tutorial","avatar":null,"date":"January 31, 2017"},{"id":"cjeqgfphbnmwk0167ldz2mv2q","title":"React onClick Example and Tutorial","slug":"/react-onclick-example-and-tutorial","avatar":null,"date":"January 2, 2017"},{"id":"cjeqgfq8ynmwu0167z6c51ynp","title":"React Tables - How To Render Tables In ReactJS","slug":"/reactjs-tables","avatar":null,"date":"January 2, 2017"},{"id":"cjeqgfuoznmxu0167ayt2zkc3","title":"How To Add A Class in ReactJS","slug":"/reactjs-add-class","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgfu6gnnrb0199nyrddra0","title":"Fetching Github Blame with the GraphQL API V4","slug":"/github-blame-graphql-api-v4","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgfsmsnnqo0199o2x7dl4x","title":"How To Add Meta Descriptions to Middleman Pages","slug":"/middleman-meta-descriptions","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgfs2wnnqi0199rco2vvz2","title":"Zapier Webhook Post Example & Tutorial","slug":"/zapier-webhook-post-example-tutorial","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgfvhunmy90167t6ouqfmb","title":"Setting Up A Job Queue For A Node App [Tutorial]","slug":"/job-queue-node","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgftbcnnqy0199nc1f92te","title":"NextJs vs Create-React-App - A Side-By-Side Comparison","slug":"/nextjs-vs-create-react-app","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgft2pnnqt01990ty8fmeu","title":"How To Create A Modal In ReactJS [Tutorial]","slug":"/reactjs-modal","avatar":null,"date":"March 14, 2018"},{"id":"cjeqgfugpnnrj0199x7anb33t","title":"How To Use Twilio With ReactJS","slug":"/reactjs-twilio-example-tutorial","avatar":null,"date":"March 14, 2018"}],"page":0,"offset":0,"total":57,"nodes":[{"id":"cjjg9ui0t303s010375m42b9w","title":"Turn a Browser Window into a Notepad with This One-Liner","slug":"turn-a-browser-window-into-a-notepad-with-this-one-liner","published_at":"2020-07-01T04:00:00","created_at":"2018-07-10T22:33:00","excerpt":"","image":{"id":"panuc7accwihsg0o0mi98io8m","url":"static/cjjg9ui0t303s010375m42b9w/OYIaJ1KK_400x400(1).png"},"posts_tags":[{"tag":{"id":"2spo553zykbgxgq4vkk3","name":"Javascript"}}],"date":"July 1, 2020","html":"
Paste the following into the location/url bar:
\ndata:text/html,<div contenteditable></div>
\n","avatar":"https://s3.amazonaws.com/contentkit/static/cjjg9ui0t303s010375m42b9w/OYIaJ1KK_400x400(1).png"},{"id":"cjiy7xl9o17w701039gvl18a5","title":"Creating a Collaborative Editor with Draftjs for Fun","slug":"draft-js-collaborative-editor","published_at":"2020-06-28T04:00:00","created_at":"2018-06-28T07:19:34","excerpt":"","image":{"id":"cjiy862wv199u01032y8co8g7","url":"static/cjiy7xl9o17w701039gvl18a5/draft-js.png"},"posts_tags":[{"tag":{"id":"1q3fvn94th4blyj7f35g","name":"React"}},{"tag":{"id":"t7vcbh4nik23ubopwib7","name":"Draft JS"}}],"date":"June 28, 2020","html":"Draft.Js is a popular rich text editor intended to be used with React. Unsurprisingly, it is also created by Facebook.
\nLike other rich text editors, Draft.js is a wrapper around contenteditable and the native Selection API. If you've ever worked with the native contenteditable and window.Selection you will know that they are a huge pain. Nick Santos wrote a very persuasive article about how contenteditable is mathematically doomed to fail.
\n\nDraftJs depends on immutable state management. This has a number of advantages. For example, since editorState is immutable, we can cheaply check whether a component should update:
\nclass extends React.Component {\n shouldComponentUpdate(nextProps, nextState) {\n return nextProps.editorState !== this.props.editorState\n }\n}\n
\nHowever immutability does not come without performance penalties. Immutability also contributes to code complexity - some things are not as easy as they should be in Draft.Js. For example, this is the code needed to insert an empty block into the editor.
\nYou've probably used Google docs. It's collaborative. If I'm editing the same document as my friend, Google Docs keeps track of and displays team member's changes simultaneously. Moreover, edits are annotated. Can the same thing be accomplished using Draft.Js?
\nHere's my first attempt (and here's the source). Note: a cookie is used, so in order for the server to detect two distinct users you'll need to open a new window in incognito or on a separate computer/browser.
\n\nHere are a few (probably obvious) observations about how this problem of collaborative editing can be tackled in DraftJs.
\nFrom the outset, we know that in order to make Draft.Js collaborative we'll need to use Web Sockets. This isn't 2006, the user shouldn't need to refresh the page to see edits.
\nSince our updates to editorState will be broadcasted over web sockets, we also know that we will need to frequently serialize and deserialize editorState. I'm talking about convertToRaw and convertFromRaw.
\nThe client is only privy to its current editorState and whatever is broadcasted from the server via WebSocket.
\nHence, in this scenario the server is the \"source of truth.\" I suppose this wouldn't differ from the conventional situation where raw editorState is stored in a database. But I think the difference is that the flow of information is bidirectional: the client updates server state and vice-versa.
\nSince the server-side editorState is deserialized (i.e., not an immutable object but just a plan ole' javascript object), we don't need to worry about computing the deltas between immutable editorStates. We can just compute the deltas between all the raw states received from the clients.
\nSo far we have:
\nIn response to an emission from the client, the server does three things:
\nThis sounds kind of rough but it's not bad at all with some the help of some third-party libraries. I used jsondiffpatch to get the deltas between states. This can be as simple as:
\n// we should wrap the raw serverState in a closure, but for brevity we'll do this\nlet rawServerState = {\n blocks: [],\n entityMap: {}\n}\n// this would come from the client via WebSocket\nlet rawClientState = {\n blocks: [{ text: 'lorem', key: 'A' }], // some keys omitted here for brevity\n entityMap: {}\n}\nlet delta = require('jsondiffpatch').diff(rawServerState, rawClientState)\n
\nThe delta can then be used to patch the server state:
\nlet patched = require('jsondiffpatch').patch(rawServerState, delta)
\nHere's a working example:
\nconst j = require('jsondiffpatch')\nconst WebSocket = require('ws')\nconst http = require('http')\nconst PORT = process.env.PORT || 1234\nconst server = http.createServer(app)\nconst wss = new WebSocket.Server({ server })\nconst createStore = () => {\n let initialState = {\n blocks: [{text: ''}],\n entityMap: {}\n }\n let state = { ...initialState }\n let users = {}\n return {\n initialState,\n getState: () => state,\n patch: (diff) => {\n state = j.patch(state, diff)\n }\n } \n}\nconst store = createStore()\nwss.on('connection', function connection (ws, req) {\n const token = req.headers.cookie && req.headers.cookie.split('=')[1]\n // When a new user connects, we need to get the delta between the server's current state\n // and the empty, initialState and emit that. The client can then patch\n // its own state when the window loads.\n const delta = j.diff(store.initialState, store.getState())\n ws.send(JSON.stringify({ delta })) \n \n // each client will broadcast its state at a regular interval if there are changes.\n ws.on('message', function incoming (data) {\n let { raw } = JSON.parse(data) \n // 1. Get the delta between the server's current state and the client-emitted state\n // note that delta will be null if there's no change.\n let delta = j.diff(store.getState(), raw)\n if (delta) {\n // 2. We need to patch the server state so that it doesn't become stale\n store.patch(delta)\n // 3. Emit the delta to all of the clients.\n wss.clients.forEach(function each (client) {\n if (client !== ws && client.readyState === WebSocket.OPEN) {\n client.send(JSON.stringify({ delta }))\n }\n })\n }\n })\n})\nserver.listen(PORT, () => {\n console.log(`listening on http://localhost:${PORT}`)\n})\n
\nHence, we are leaning heavily on jsondiffpatch to do the heavy lifting.
\nNow let's look at the client side.
\nimport React from 'react'\nimport { Editor, EditorState, convertToRaw, convertFromRaw } from 'draft-js'\nimport debounce from 'debounce'\nimport j from 'jsondiffpatch'\nclass CollaborativeEditor extends React.Component {\n state = {\n editorState: EditorState.createEmpty()\n }\n \n _isUnmounted = false\n \n componentDidMount () {\n let host = window.location.origin.replace(/^http/, 'ws')\n this.ws = new window.WebSocket(host)\n this.ws.onmessage = (event) => {\n if (this._isUnmounted) return\n this.handleMessage(JSON.parse(event.data))\n }\n }\n \n componentWillUnmount () {\n this.ws.close()\n delete this.ws\n this._isUnmounted = true\n }\n \n handleMessage = ({ delta }) => {\n if (!delta) return\n let raw = convertToRaw(this.state.editorState.getCurrentContent())\n let nextContentState = convertFromRaw(j.patch(raw, delta))\n this.setState({\n editorState: EditorState.push(editorState, nextContentState)\n })\n }\n \n broadcast = debounce((editorState) => {\n if (!this.ws) return\n this.ws.send(JSON.stringify({\n raw: convertToRaw(editorState.getCurrentContent())\n }))\n }, 300)\n \n onChange = editorState => {\n this.broadcast(editorState)\n this.setState({ editorState })\n }\n render () {\n return (\n <Editor \n editorState={this.state.editorState}\n onChange={this.onChange}\n />\n )\n }\n}
\n\nAs you might expect, we:
\ncomponentDidMount
and componentWillUnmount
lifecycle hooks to manage the client WebSocket connection.this.broadcast
. The broadcast class method is debounced for obvious reasons (to avoid flooding the channel). We broadcast to the server the client's current raw, serialized state ever 300 ms using the third-party library debounce
.jsondiffpatch
) and then unmarshal the result to rehydrate the client-side editorState. In part two, we'll discuss selection state bookkeeping in collaborative editing. For example, we want to attribute another user's edits to a document to that user rather than the current, editing user.
","avatar":"https://s3.amazonaws.com/contentkit/static/cjiy7xl9o17w701039gvl18a5/draft-js.png"},{"id":"cjeqgfsdsnmx90167n7j290kt","title":"How To Include SASS In Your React Project","slug":"sass-react-webpack","published_at":"2020-05-01T04:00:00","created_at":"2018-03-14T02:16:41","excerpt":"Styling components in React has gotten a bit weird recently.","image":{"id":"cjiox9pg65d7y0103zlovq1s3","url":"static/cjeqgfsdsnmx90167n7j290kt/parcel-bundler.png"},"posts_tags":[{"tag":{"id":"1q3fvn94th4blyj7f35g","name":"React"}}],"date":"May 1, 2020","html":"Styling components in React has gotten a bit weird recently.
\n\nRemember back in the day when people would talk about the separation of style (css), content (html) and logic (js, php etc)? This was used as an argument against inline styles like the following:
\n<span style=\"color: red;\">Hello world</span>
\nWith React, styles are often commingled with component logic:
\nconst styles = {\n color: \"red\"\n};\nconst HelloWorld = () = (\n <div style={styles}>Hello world</div>\n)
\nBut you can achieve some degree of separation by keeping presentational, pure components separate from container components.
\nYou can use parcel-bundler to bundle React projects without any configuration.
\nnpm i parcel-bundler --save\nnpm i babel-preset-env babel-preset-react babel-plugin-transform-runtime babel-plugin-transform-class-properties --dev\n
\nThen you can include sass by simply importing it:
\n// Component.js\nimport React from 'react'\nimport './styles.scss';\nclass MyComponent extends React.Component { ... }\n
\nWhen building, parcel-bundler autodetects sass imports and will automatically install node-sass.
\nI'm a big fan of SASS. The idea of programmatic CSS is pretty sexy.
\nHere's how to use SASS with React and Webpack in three easy steps.
\nnpm i sass-loader node-sass webpack --save-dev
\nYou'll also need style-loader and css-loader:
\nnpm i style-loader css-loader --save-dev
\nmodule.exports = {\n entry: \"./app/entry\", // string | object | array\n // Here the application starts executing\n // and webpack starts bundling\n output: {\n // options related to how webpack emits results\n path: path.resolve(__dirname, \"dist\"), // string\n // the target directory for all output files\n // must be an absolute path (use the Node.js path module)\n filename: \"bundle.js\", // string\n // the filename template for entry chunks\n },\n module: {\n rules: [{\n test: /\\.scss$/,\n use: [{\n loader: \"style-loader\"\n }, {\n loader: \"css-loader\" \n }, {\n loader: \"sass-loader\"\n }]\n }]\n }
\nmodule: { rules: [{ test: /.scss$/ }] }
. Webpack uses a regular expression to determine which files it should look for and serve to a specific loader. In this case any file that ends with .scss will be served to sass-loader first (sass to css), then css-loader, and finally style-loader.Usually, you'd include the SASS file in the entry component for your app.
\nimport React from 'react'\nimport './style.scss'\nclass App extends React.Component {\n render() {\n return(<div>Hello World</div>)\n }\n}
\nThis is a assuming you have a directory structure like this:
\n|--app.js\n|--style.scss\n|--components/\n|--containers/
\nPer usual, in style.scss you can import your other sass files like so:
\n@import 'buttons';\n@import 'modal';
\nAnd so on.
\nvar path = require('path');\nmodule.exports = {\n entry: './src/index.js',\n output: {\n filename: 'bundle.js',\n path: path.resolve(__dirname, 'dist')\n },\n module: {\n rules: [{\n test: /\\.css$/,\n use: [\n 'style-loader',\n 'css-loader'\n ]\n }]\n }\n}
","avatar":"https://s3.amazonaws.com/contentkit/static/cjeqgfsdsnmx90167n7j290kt/parcel-bundler.png"},{"id":"cjiy4tseq0tnw0103bc5s7sxj","title":"How to Add a Loading Indicator to Material Ui's Component","slug":"creating-a-material-ui-button-with-spinner-that-reflects-loading-state","published_at":"2020-01-28T05:00:00","created_at":"2018-06-28T05:52:38","excerpt":"","image":{"id":"cjiy78moi13zi0103ea38bbjq","url":"static/cjiy4tseq0tnw0103bc5s7sxj/material-ui.png"},"posts_tags":[{"tag":{"id":"v2lazbg2ge2blz4cna6g","name":"Material UI"}}],"date":"January 28, 2020","html":"Users are less likely to get frustrated with a website if they get feedback that \"work\" is being done. Hence the ubiquity of progress indicators on the web. This is actually the original inspiration for Node.js:
\nDahl was inspired to create Node.js after seeing a file upload progress bar on Flickr. The browser did not know how much of the file had been uploaded and had to query the Web server. Dahl desired an easier way.\n
When a user submits a login form in the authentication flow it's desirable to give the user feedback that the request is in flight.
\n\nMaterial UI is an implementation of Google's material design in React.
\nMaterial UI has some fancy buttons. Let's make them even fancier by adding a spinner (progress indicator) that's activated when the user submits a login form and inactivated when authentication is complete.
\nOK, here's our button:
\nimport React from 'react'\nimport Button from '@material-ui/core/Button'\nconst LoginButton = (props) => {\n const {\n children,\n loading,\n ...rest\n } = props\n return (\n <Button {...rest}>\n {children}\n </Button>\n )\n}\n
\nNow let's add a progress indicator. Fortunately, Material UI has many such indicators to choose from.
\nimport React from 'react'\nimport Button from '@material-ui/core/Button'\nimport CircularProgress from '@material-ui/core/CircularProgress'\nconst styles = {\n root: {\n marginLeft: 5\n }\n}\nconst SpinnerAdornment = withStyles(styles)(props => (\n <CircularProgress\n className={props.classes.spinner}\n size={20}\n />\n))\nconst AdornedButton = (props) => {\n const {\n children,\n loading,\n ...rest\n } = props\n return (\n <Button {...rest}>\n {children}\n {loading && <SpinnerAdornment {...rest} />}\n </Button>\n )\n}\n
\nNow when authentication is in progress, we just need to set loading={true} and pass that as a prop to
Here's an example of what our parent component might look like:
\nimport React from 'react'\nimport TextField from '@material-ui/core/TextField'\nimport { withStyles } from '@material-ui/core/styles'\nimport { unstable_deferredUpdates as deferredUpdates } from 'react-dom'\nconst styles = theme => ({\n container: {\n position: 'absolute',\n width: '100%'\n },\n login: {\n boxShadow: '0 15px 35px rgba(50,50,93,.1), 0 5px 15px rgba(0,0,0,.07)',\n borderRadius: '4px',\n padding: '5vh',\n backgroundColor: '#fff',\n maxWidth: '340px',\n margin: '25vh auto',\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between'\n },\n row: {\n display: 'flex',\n flexDirection: 'row',\n justifyContent: 'space-between'\n },\n button: {\n maxWidth: '200px',\n margin: theme.spacing.unit\n },\n gutter: {\n marginBottom: '1em'\n }\n})\nclass LoginForm extends React.Component {\n state = {\n email: '',\n password: '',\n loading: false\n }\n onChange = (key, { currentTarget }) => {\n this.setState({\n [key]: currentTarget.value\n })\n }\n \n loginOrCreateAccount = () => {\n if (this.state.loading) return\n deferredUpdates(() => {\n this.setState({ loading: true })\n })\n this.props.createAccountSomehow(this.state)\n .then(() => {\n deferredUpdates(() => {\n this.setState(prevState => prevState.loading\n ? { loading: false } : null\n })\n })\n }\n render () {\n return (\n <div>\n <form className={classes.login}>\n <div className={classes.gutter}>\n <TextField\n id='email'\n label='Email'\n value={this.state.email}\n onChange={evt => this.onChange('email', evt)}\n autoComplete='current-email'\n />\n <TextField\n id='password'\n fullWidth\n value={this.state.password}\n onChange={evt => this.onChange('password', evt)}\n placeholder='Password'\n margin='normal'\n autoComplete='current-password'\n />\n </div>\n <div className={classes.row}>\n <AdornedButton\n className={classes.button}\n fullWidth\n loading={this.state.loading}\n variant='flat'\n color='secondary'\n id='submit-login'\n onClick={this.loginOrCreateAccount}\n >\n Sign in\n </AdornedButton>\n <AdornedButton\n className={classes.button}\n fullWidth\n loading={this.state.loading}\n variant='raised'\n color='primary'\n onClick={this.loginOrCreateAccount}\n >\n Create account\n </AdornedButton>\n </div>\n </form>\n </div>\n )\n }\n}\nexport default withStyles(styles)(LoginForm)\n
\nWe've used the AdornedButton from the second snippet twice: once for the login button and once for the create account button.
\nThe overall strategy is straightforward: the user clicks one of two buttons (Login or Create Account), invoking this.loginOrCreateAccount(). Next, we toggle the loading state to true, authenticate the user, and toggle the loading the state back to false.
\n\nSuch aggressive setState() calls in one class method really flogs the render cycle. It's not ideal.
\nHence, we use unstable_deferredUpdates. unstable_deferredUpdates is an experimental feature React recently added. It's exported by the package react-dom starting with react-dom@16.4.2.
\nunstable_deferredUpdates accepts a function with a setState call. The intended use-case is a low priority update.
\n// Here's some annotated pseudo-code that may or may not be illuminating\nclass extends React.Component {\n ...\n loginOrCreateAccount = () => {\n // line below prevents concurrent requests\n if (this.state.loading) return\n // We set the loading state to true here.\n // This update is of course asynchronous.\n // I chose not to await the state change \n // because it may be more performant to start \n // authenticating the user at the same time.\n deferredUpdates(() => {\n this.setState({ loading: true })\n })\n this.props.createAccountSomehow(this.state)\n .then(() => {\n // Now we toggle the loading state back to false.\n // setState() accepts a function in addition to an object.\n // If a function is supplied, it is called with the most \n // recent version of this.state. \n // Returning null inside a functional setState call is \n // an escape hatch that lets us avoid updating state.\n deferredUpdates(() => {\n this.setState(prevState => prevState.loading\n ? { loading: false } : null\n })\n })\n }\n }
\nHere's the result:
\n\nAnd an implementation of the examples above in the wild: https://github.com/unshift/contentkit/blob/master/src/containers/Login/LoginPage.js
\n","avatar":"https://s3.amazonaws.com/contentkit/static/cjiy4tseq0tnw0103bc5s7sxj/material-ui.png"},{"id":"cjkq4u7470ihr0157ad175b12","title":"Connecting to AWS Lambda via WebSockets","slug":"connecting-to-aws-lambda-via-websockets","published_at":"2020-01-12T05:00:00","created_at":"2018-08-12T00:50:12","excerpt":"","image":{"id":"cjkvp9ora072l01219ttici63","url":"static/cjkq4u7470ihr0157ad175b12/MQTT.js.png"},"posts_tags":[{"tag":{"id":"3pl7ejawubufz70vjnad","name":"AWS"}}],"date":"January 12, 2020","html":"
AWS Lambda is usually used for short-lived processes like the request/response lifecycle. The default Lambda timeout of 3 seconds reflects this common use-case.
\n\nHowever the maximum execution duration of AWS Lambda functions was increased from 1 minute to 5 minutes a few years ago.
\nNot only is it possible to connect to AWS Lambda via WebSockets using MQTT or AWS IoTData but this strategy works surprisingly well.
\nMQTT is both a general publish-subscribe messaging protocol and a popular npm package. MQTT (the npm package) has a clean interface that enables initiating a WebSocket connection, subscribing to topics, listening for messages, and publishing messages.
\nOn the client side, we fetch a WebSocket url that formally looks something like this:
\nwss://XXXXX.iot.us-east-1.amazonaws.com/mqtt?X-Amz-Date=20180812T133259Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ZZZ%2F20180812%2Fus-east-1%2Fiotdevicegateway%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=PPP&X-Amz-Security-Token=QQQ
\nThis can be generated using aws-sign-mqtt:
\n// set the AWS_IOT_HOST environmental variable:\n// export AWS_IOT_HOST=$(aws --region us-east-1 iot describe-endpoint --output text)\nconst url = require('aws-sign-mqtt')()
\nThis is just a WebSocket URL signed using AWS signature v4.
\nThis WebSocket URL should obviously be generated server-side (lest you expose your AWS credentials).
\nTo demonstrate how AWS Lambda can be used for long-lived WebSocket connections, we'll create two simple Lambda functions. The first is a simple HTTP endpoint that returns a signed WebSocket url. The second lambda function is triggered when the client subscribes to an AWS IoT topic.
\nconst AWS = require('aws-sdk')\nconst createPresignedURL = require('aws-sign-mqtt')\nconst { randomBytes } = require('crypto')\n// Lambda function #1 - session\n// Simple HTTP endpoint that returns the WebSocket URL and channel ID\nmodule.exports.session = (event, context, callback) => {\n const endpointUrl = createPresignedURL()\n // create an arbitrary channel ID to identify the current session\n const channelId = randomBytes(5).toString('hex') \n callback(null, {\n statusCode: 200,\n body: JSON.stringify({ endpointUrl, channelId }),\n headers: {\n 'Access-Control-Allow-Origin': '*'\n }\n })\n}\n// Lambda function #2 - mqtt\n// Sends messages to the client via AWS IotData.\n// You must manually set the AWS_IOT_HOST environmental variable which is account-specific.\n// From the command line run: aws iot describe-endpoint --output text\nmodule.exports.mqtt = async (payload, context) => {\n // The payload is whatever we send from the browser\n const { channelId } = payload\n const iot = new AWS.IotData({\n endpoint: process.env.AWS_IOT_HOST,\n region: process.env.AWS_REGION\n })\n // we respond with a different topic that uses the channelID\n // to avoid broadcasting the same message to all users\n await iot.publish({\n topic: `foobar/${channelId}/response`,\n payload: JSON.stringify({ message: 'hello' }),\n qos: 1\n }).promise()\n // at this point the Lambda function ends because the Node event loop is closed\n}\n
\nHere's what our serverless.yml might look like:
\nservice: foobar\nprovider:\n name: aws\n runtime: nodejs8.10\n region: us-east-1\n environment: \n AWS_IOT_HOST: ''\n iamRoleStatements:\n - Effect: \"Allow\"\n Action: \n - iot:*\n Resource: \"*\"\nfunctions:\n session:\n handler: handler.session\n events:\n - http:\n method: GET\n path: /\n cors: true # change this in production to \"credentails: true\" to secure endpoint\n mqtt:\n handler: handler.mqtt\n events: \n - iot\n sql: 'SELECT * FROM foobar/session'\n
\nAfter deploying our function (sls deploy), serverless will print the session endpoint that we need to generate the WebSocket URL:
\nService Information\nservice: foobar\nregion: us-east-1\nstack: foobar-dev\napi keys:\n None\nendpoints:\n GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev\n GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev\nfunctions:\n mqtt: foobar-dev-mqtt\n session: foobar-dev-session
\nIf you have the AWS command line interface installed you can try running something like the following from terminal to test your functions:
\naws --region us-east-1 iot-data publish --topic 'foobar/session' --payload '{ \"channelId\": \"1\" }'
\nThe above command should trigger foobar-dev-mqtt.
\nCopy the HTTP endpoint (https://xxxxx.execute-api.us-east-1.amazonaws.com/dev) to use in the client-side code.
\nIn the browser, we fetch the WebSocket URL and initiate a connection using mqtt. Here's an example:
\n// client.js\nconst mqtt = require('mqtt')\nconst ENDPOINT_URL = 'https://xxxxx.execute-api.us-east-1.amazonaws.com/dev'\nasync function connect () {\n // fetch URL and arbitrary channelId from Lambda function #1 (session)\n let { channelId, endpointUrl } = await fetch(ENDPOINT_URL)\n .then(resp => resp.json())\n let channel = mqtt.connect(endpointUrl)\n channel.on('connect', () => {\n channel.subscribe(`foobar/${channelId}/response`, () => {\n // publish a message to the 'foobar/session' topic \n // which triggers Lambda function #2 (mqtt)\n channel.publish('foobar/session', {\n payload: JSON.stringify({ channelId }),\n qos: 1,\n })\n // Listen for messages\n channel.on('message', (topic, buffer) => {\n console.log({ topic, message: buffer.toString() })\n })\n })\n })\n}\nconnect()
\nSo far we have deployed two lambda functions: the first generates a signed WebSocket URL and the second responds by publishing messages using AWS.IotData.
\nHowever, responding in AWS Lambda using IotData is limited because we can only send one message from the client (the first message that initiates the session). What if we want to respond to a stream of messages? In that case, we'll use the MQTT package on the server side as well.
\nOur new version of Lambda function #2 (mqtt) might look like this:
\n// handler.js\nconst mqtt = require('mqtt')\nconst createPresignedURL = require('aws-sign-mqtt')\nconst connect = ({ channel }) =>\n new Promise((resolve, reject) => channel.on('connect', resolve))\nconst subscribe = ({ channel, topic }) =>\n new Promise((resolve, reject) => channel.subscribe(topic, resolve))\n \nmodule.exports.mqtt = async (payload, context, callback) => {\n const { channelId } = payload\n const TOPIC_REQUEST = `foobar/${channelId}/request`\n const TOPIC_RESPONSE = `foobar/${channelId}/response`\n const channel = mqtt.connect(createPresignedURL())\n await connect({ channel })\n await subscribe({ channel, topic: TOPIC_REQUEST })\n let resolve\n let promise = new Promise(res => {\n resolve = res\n })\n const sessionTimeout = () => setTimeout(() => {\n channel.end(resolve)\n }, 30000)\n let timeout = sessionTimeout()\n channel.on('message', (topic, buffer) => {\n console.log({ topic, message: buffer.toString() })\n clearTimeout(timeout)\n timeout = sessionTimeout()\n channel.publish(TOPIC_RESPONSE, {\n qos: 1,\n payload: JSON.stringify({ message: 'PONG' })\n })\n })\n // this promise is resolved after 30 seconds of inactivity\n // ending function execution. If we don't promisify these event\n // handlers, then the Lambda function will end prematurely.\n await promise.then(() => callback(null))\n}
\nThe primary difference between this version and what we had before is that now we're using MQTT to listen for messages and respond rather than AWS.IotData. This is necessary for two-way communication between client and server.
\nWe can then revise our client-side code as follows:
\n// client.js\nconst mqtt = require('mqtt')\nconst ENDPOINT_URL = 'https://xxxxx.execute-api.us-east-1.amazonaws.com/dev'\nasync function connect () {\n // fetch URL and arbitrary channelId from Lambda function #1 (session)\n let { endpointUrl, channelId } = await fetch(ENDPOINT_URL)\n .then(resp => resp.json())\n let channel = mqtt.connect(endpointUrl)\n channel.on('connect', () => {\n channel.subscribe(`foobar/${channelId}/response`, async () => {\n // publish a message to the 'foobar/session' topic \n // which triggers Lambda function #2 (mqtt)\n await new Promise((resolve, reject) => \n channel.publish(\n 'foobar/session', {\n payload: JSON.stringify({ channelId }),\n qos: 1,\n },\n resolve\n )\n )\n \n // Listen for messages\n channel.on('message', (topic, buffer) => {\n console.log({ topic, message: buffer.toString() })\n })\n \n channel.publish(\n `foobar/${channelId}/request`,\n JSON.stringify({ message: 'hello' }),\n { qos: 1 }\n )\n })\n })\n}\nconnect()
\nWhat's different? This time we issue a request using the topic foobar/{channelId}/request. As before, we listen for messages using the topic foobar/{channelId}/response.
\nWhen we run the above client-side code, here's what happens:
\nSometimes it's best to look at the source code of real projects rather than muddle through half-baked tutorials. To that end, here are some examples:
\n