Skip to content

Commit

Permalink
add useMountFetch hook
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Bernard <alexandre.bernard@epita.fr>
  • Loading branch information
Alexandre Bernard committed Apr 19, 2020
1 parent fa933ba commit 2c95a7c
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
112 changes: 112 additions & 0 deletions src/useMountFetch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useState, useEffect } from 'react'

// Helpers
import ScraperWraper from './rules'
import { makeCancelable } from './utils'

// Types
import { IReactTinyLinkData } from './ReactTinyLinkTypes'

/** current state of the fetch */
export interface IState<T,E> {
/**
* The respones to the request. Undefined if the request not done or returned
* an error.
*/
response: T|undefined

/** Loading status. `true` if loading, `false` otherwise. */
loading: boolean,

/**
* Error status. Undefined if the request returned a valid response or is
* loading.
*/
error: E|undefined
}

/** array composed of:
* 1. The respones to the request. Undefined if the request not done or returned
* an error.
* 2. The loading status. `true` if loading, `false` otherwise.
* 3. Ther error status. Undefined if the request returned a valid response or is
* loading.
*/
export type ResponseState<T,E> = [T|undefined, boolean, E|undefined]

export function useMountFetch(
url: string,
proxyUrl: string,

/** default medias passed to the `ScraperWraper` */
defaultMedias: string[],

/**
* Permits to pass a default value. This will be the response's value
* during the loading and in case of error.
*/
defaultValue?: IReactTinyLinkData,

// Hooks for the caller
/** Called when the fetch failed with the reason of the failure */
onError?: (error: Error) => void,

/** Called when the fetch succeeded with the resulting data */
onSuccess?: (response: IReactTinyLinkData) => void,
): ResponseState<IReactTinyLinkData, Error>
{
// Alias to IState
type State = IState<IReactTinyLinkData, Error>

// Setup the state
const [state, setState] = useState<State>({
response: defaultValue,
loading: true, // Avoid a re-render on useEffect
error: undefined
})

const client = fetch(proxyUrl ? `${proxyUrl}/${url}` : url, {
headers: {
'x-requested-with': '',
},
})

/** Does the fetch on mount. Ensure the cleanup in case of premature unmounting */
useEffect(() => {
// Permit to control if should set the state. Avoiding a memory leak
let isMounted: boolean = true

// Wraps the `ScraperWraper` to manage the hook's state
const doFetch = async (): Promise<IReactTinyLinkData> => {
const finalStateUpdate: Partial<State> = {loading: false, error: undefined}

try {
const data = await ScraperWraper(url, client, defaultMedias)
finalStateUpdate.response = data

onSuccess && isMounted && onSuccess(data)
return data
} catch (err) {
finalStateUpdate.error = err
onError && isMounted && onError(err)

return err
} finally {
isMounted && setState(old => ({...old, ...finalStateUpdate}))
}
}

// Makes the request and wraps it so we can cancel it if needed
const cancelable = makeCancelable(doFetch())

// Returns a cleanup function which permits to avoid potential
// memory leaks and unnecessary network when the component is
// unmount.
return () => {
isMounted = false // Avoid all the state management
cancelable.cancel() // Cancel the request
}
}, []) // Put no dependecy, does the fetch only once on mount

return [state.response, state.loading, state.error]
}
48 changes: 48 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { css } from 'styled-components';
import { IReactTinyLinkData, ReactTinyLinkType } from './ReactTinyLinkTypes';

const REGEX_STRIP_WWW = /^www\./;
export const media = {
Expand Down Expand Up @@ -35,3 +36,50 @@ export const findFirstSecureUrl = (records, condition) => {
const result = records.find(record => condition(record) && record.startsWith('https://'));
return result ? result : '';
}

/**
* @returns {IReactTinyLinkData} with default values.
*
* @param url url to fetch
* @param defaultMedia media assigned to both the image and the video
*/
export function defaultData(url: string, defaultMedia: string[]): IReactTinyLinkData {
return {
title: url.substring(url.lastIndexOf('/') + 1),
description: url.substring(url.lastIndexOf('/') + 1),
image: defaultMedia,
url: url,
video: defaultMedia,
type: ReactTinyLinkType.TYPE_DEFAULT,
}
}

/** does nothing. Just here to have a constance reference */
export function noop() {
return
}

/**
* Wraps a promise to make it cancelable.
*
* from https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
* defined by @istarkov at:
* https://github.com/facebook/react/issues/5465#issuecomment-157888325
*/
export const makeCancelable = (promise) => {
let hasCanceled_ = false;

const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});

return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};

0 comments on commit 2c95a7c

Please sign in to comment.