From 223bccd6b1c79591bcc9375f4bd787f50ae37eba Mon Sep 17 00:00:00 2001 From: Michael Fix Date: Tue, 5 Mar 2019 11:58:29 -0800 Subject: [PATCH] read all api calls from context (#691) --- .eslintrc.js | 3 ++- components/ApiContext.js | 4 ++++ components/Editor.js | 15 ++++++++------- components/ImagePicker.js | 7 +++++-- components/RandomImage.js | 34 ++++++++-------------------------- components/TweetButton.js | 12 +++++++++--- lib/api.js | 33 ++++++++++++++++++++++++++++++--- package.json | 2 +- pages/index.js | 8 +------- yarn.lock | 8 ++++---- 10 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 components/ApiContext.js diff --git a/.eslintrc.js b/.eslintrc.js index 14b3a0d..da8064d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,7 +23,8 @@ module.exports = { 'react/jsx-uses-react': 'error', 'react/jsx-uses-vars': 'error', 'jsx-a11y/click-events-have-key-events': 'off', - 'react-hooks/rules-of-hooks': 'error' + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error' }, settings: { react: { diff --git a/components/ApiContext.js b/components/ApiContext.js new file mode 100644 index 0000000..a2950ac --- /dev/null +++ b/components/ApiContext.js @@ -0,0 +1,4 @@ +import React from 'react' +import api from '../lib/api' + +export default React.createContext(api) diff --git a/components/Editor.js b/components/Editor.js index d84a20d..b56b86f 100644 --- a/components/Editor.js +++ b/components/Editor.js @@ -6,6 +6,7 @@ import dynamic from 'next/dynamic' import Dropzone from 'dropperx' // Ours +import ApiContext from './ApiContext' import Dropdown from './Dropdown' import Settings from './Settings' import Toolbar from './Toolbar' @@ -38,6 +39,7 @@ const BackgroundSelect = dynamic(() => import('./BackgroundSelect'), { }) class Editor extends React.Component { + static contextType = ApiContext constructor(props) { super(props) this.state = { @@ -65,8 +67,8 @@ class Editor extends React.Component { const initialState = Object.keys(queryParams).length ? queryParams : {} try { // TODO fix this hack - if (this.props.api.getGist && path.length >= 19 && path.indexOf('.') === -1) { - const { content, language } = await this.props.api.getGist(path) + if (this.context.gist && path.length >= 19 && path.indexOf('.') === -1) { + const { content, language } = await this.context.gist.get(path) if (language) { initialState.language = language.toLowerCase() } @@ -115,9 +117,9 @@ class Editor extends React.Component { ) { // if safari, get image from api const isPNG = format !== 'svg' - if (this.props.api.image && this.isSafari && isPNG) { + if (this.context.image && this.isSafari && isPNG) { const encodedState = serializeState(this.state) - return this.props.api.image(encodedState) + return this.context.image(encodedState) } const node = this.carbonNode.current @@ -226,7 +228,7 @@ class Editor extends React.Component { upload() { this.getCarbonImage({ format: 'png' }).then( - this.props.api.tweet.bind(null, this.state.code || DEFAULT_CODE) + this.context.tweet.bind(null, this.state.code || DEFAULT_CODE) ) } @@ -322,7 +324,7 @@ class Editor extends React.Component { getCarbonImage={this.getCarbonImage} />
- {this.props.api.tweet && } + {}, onReset: () => {} } diff --git a/components/ImagePicker.js b/components/ImagePicker.js index 215b7e6..c43e928 100644 --- a/components/ImagePicker.js +++ b/components/ImagePicker.js @@ -1,11 +1,12 @@ import React from 'react' import ReactCrop, { makeAspectCrop } from 'react-image-crop' -import RandomImage, { downloadThumbnailImage } from './RandomImage' +import RandomImage from './RandomImage' import PhotoCredit from './PhotoCredit' import Input from './Input' import { Link } from './Meta' import { fileToDataURL } from '../lib/util' +import ApiContext from './ApiContext' const getCroppedImg = (imageDataURL, pixelCrop) => { const canvas = document.createElement('canvas') @@ -43,6 +44,7 @@ const INITIAL_STATE = { } export default class ImagePicker extends React.Component { + static contextType = ApiContext constructor(props) { super(props) this.state = INITIAL_STATE @@ -103,7 +105,8 @@ export default class ImagePicker extends React.Component { handleURLInput(e) { e.preventDefault() const url = e.target[0].value - return downloadThumbnailImage({ url }) + return this.context + .downloadThumbnailImage({ url }) .then(({ dataURL }) => this.props.onChange({ backgroundImage: dataURL, diff --git a/components/RandomImage.js b/components/RandomImage.js index 214d9cd..b37d6fb 100644 --- a/components/RandomImage.js +++ b/components/RandomImage.js @@ -2,47 +2,29 @@ import React from 'react' import Spinner from 'react-spinner' import { useAsyncCallback } from '@dawnlabs/tacklebox' -import api from '../lib/api' - +import ApiContext from './ApiContext' import PhotoCredit from './PhotoCredit' -import { fileToDataURL } from '../lib/util' - -export const downloadThumbnailImage = img => { - return api.client - .get(img.url.replace('http://', 'https://'), { responseType: 'blob' }) - .then(res => res.data) - .then(fileToDataURL) - .then(dataURL => Object.assign(img, { dataURL })) -} - -const getImageDownloadUrl = img => - api.client.get(`/unsplash/download/${img.id}`).then(res => res.data.url) - -async function getImages() { - const imageUrls = await api.client.get('/unsplash/random') - return Promise.all(imageUrls.data.map(downloadThumbnailImage)) -} function RandomImage(props) { const { current: cache } = React.useRef([]) const [cacheIndex, updateIndex] = React.useState(0) + const api = React.useContext(ApiContext) const [selectImage, { loading: selecting }] = useAsyncCallback(() => { const image = cache[cacheIndex] - return getImageDownloadUrl(image) - .then(url => api.client.get(url, { responseType: 'blob' })) - .then(res => res.data) - .then(blob => props.onChange(blob, image)) + return api.unsplash.download(image.id).then(blob => props.onChange(blob, image)) }) - const [updateCache, { loading: updating, data: imgs }] = useAsyncCallback(getImages) + const [updateCache, { loading: updating, error, data: imgs }] = useAsyncCallback( + api.unsplash.random + ) React.useEffect(() => { - if (cacheIndex === 0 || cacheIndex > cache.length - 2) { + if (!error && !updating && (!imgs || cacheIndex > cache.length - 2)) { updateCache() } - }, [cacheIndex, cache.length, updateCache]) + }, [error, updating, imgs, cacheIndex, cache.length, updateCache]) React.useEffect(() => { if (imgs) { diff --git a/components/TweetButton.js b/components/TweetButton.js index 437f7d6..8a12ca9 100644 --- a/components/TweetButton.js +++ b/components/TweetButton.js @@ -1,14 +1,15 @@ import React from 'react' import { useAsyncCallback } from '@dawnlabs/tacklebox' +import ApiContext from './ApiContext' import Button from './Button' function useWindowListener(key, fn) { - const callback = React.useRef(fn) + const { current: callback } = React.useRef(fn) React.useEffect(() => { - window.addEventListener(key, callback.current) - return () => window.removeEventListener(key, callback.current) + window.addEventListener(key, callback) + return () => window.removeEventListener(key, callback) }, [key, callback]) } @@ -30,9 +31,14 @@ function useOnlineListener() { } function TweetButton(props) { + const api = React.useContext(ApiContext) const online = useOnlineListener() const [onClick, { loading }] = useAsyncCallback(props.onClick) + if (!api || !api.tweet) { + return null + } + if (!online) { return null } diff --git a/lib/api.js b/lib/api.js index 9115323..d836288 100644 --- a/lib/api.js +++ b/lib/api.js @@ -2,6 +2,8 @@ import axios from 'axios' import debounce from 'lodash.debounce' import ms from 'ms' +import { fileToDataURL } from './util' + const client = axios.create({ baseURL: `${ process.env.API_URL || process.env.NODE_ENV === 'production' ? '' : 'http://localhost:4000' @@ -68,9 +70,34 @@ function checkIfRateLimited(err) { throw err } +const downloadThumbnailImage = img => { + return client + .get(img.url.replace('http://', 'https://'), { responseType: 'blob' }) + .then(res => res.data) + .then(fileToDataURL) + .then(dataURL => Object.assign(img, { dataURL })) +} + +const unsplash = { + download(id) { + return client + .get(`/unsplash/download/${id}`) + .then(res => res.data.url) + .then(url => client.get(url, { responseType: 'blob' })) + .then(res => res.data) + }, + async random() { + const imageUrls = await client.get('/unsplash/random') + return Promise.all(imageUrls.data.map(downloadThumbnailImage)) + } +} + export default { - client, - getGist, + gist: { + get: getGist + }, tweet: debounce(tweet, ms('5s'), { leading: true, trailing: false }), - image: debounce(image, ms('5s'), { leading: true, trailing: false }) + image: debounce(image, ms('5s'), { leading: true, trailing: false }), + unsplash, + downloadThumbnailImage } diff --git a/package.json b/package.json index 9d90433..ea11144 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "eslint-plugin-import": "^2.16.0", "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-react": "^7.12.3", - "eslint-plugin-react-hooks": "^1.1.0-rc.0", + "eslint-plugin-react-hooks": "^1.4.0", "husky": "^1.3.1", "lint-staged": "^8.1.3", "now": "^14.0.0", diff --git a/pages/index.js b/pages/index.js index 440f730..47cff72 100644 --- a/pages/index.js +++ b/pages/index.js @@ -7,7 +7,6 @@ import debounce from 'lodash.debounce' import Editor from '../components/Editor' import Page from '../components/Page' import { MetaLinks } from '../components/Meta' -import api from '../lib/api' import { updateQueryString } from '../lib/routing' import { saveSettings, clearSettings, omit } from '../lib/util' @@ -30,12 +29,7 @@ class Index extends React.Component { return ( - + ) } diff --git a/yarn.lock b/yarn.lock index 9608a3c..11d3aab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2722,10 +2722,10 @@ eslint-plugin-jsx-a11y@^6.2.1: has "^1.0.3" jsx-ast-utils "^2.0.1" -eslint-plugin-react-hooks@^1.1.0-rc.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.2.0.tgz#a1c78e792b8d7d3e9c2a2aad28df80b9b5cd1101" - integrity sha512-pb/pwyHg0K3Ss/8loSwCGRSXIsvPBHWfzcP/6jeei0SgWBOyXRbcKFpGxolg0xSmph0jQKLyM27B74clbZM/YQ== +eslint-plugin-react-hooks@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.4.0.tgz#ad86001e05519368e55a888b1b31004b8e2ae8f6" + integrity sha512-fMGlzztW/5hSQT0UBnlPwulao0uF8Kyp0Uv6PA81lzmcDz2LBtthkWQaE8Wz2F2kEe7mSRDgK8ABEFK1ipeDxw== eslint-plugin-react@^7.12.3: version "7.12.4"