Skip to content

Commit

Permalink
Rewrite with Typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlaz committed Mar 17, 2019
1 parent 46e22b4 commit 5b57376
Show file tree
Hide file tree
Showing 23 changed files with 454 additions and 349 deletions.
16 changes: 0 additions & 16 deletions .babelrc.js

This file was deleted.

3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

26 changes: 0 additions & 26 deletions .eslintrc

This file was deleted.

36 changes: 20 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"author": "Netcetera AG",
"license": "MIT",
"repository": "https://github.com/netceteragroup/react-message-source",
"main": "dist/index.js",
"module": "dist/index.es.js",
"jsnext:main": "dist/index.es.js",
"main": "dist/react-message-source.js",
"module": "dist/react-message-source.es.js",
"jsnext:main": "dist/react-message-source.es.js",
"engines": {
"node": ">=8",
"npm": ">=5"
Expand All @@ -23,6 +23,7 @@
"test": "cross-env CI=1 react-scripts test --env=jsdom",
"test:watch": "react-scripts test --env=jsdom",
"coverage": "cross-env CI=1 react-scripts test --coverage --coverageReporters=text-lcov | coveralls",
"prebuild": "rimraf dist",
"build": "rollup -c",
"start": "rollup -c -w",
"prepare": "yarn run build",
Expand All @@ -37,31 +38,34 @@
"react": "^16.8.0"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@types/hoist-non-react-statics": "^3.3.0",
"@types/invariant": "^2.2.29",
"@types/jest": "^24.0.11",
"@types/react": "^16.8.8",
"@types/react-dom": "^16.8.2",
"@types/react-test-renderer": "^16.8.1",
"coveralls": "^3.0.3",
"cross-env": "^5.2.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^1.5.1",
"prettier": "^1.16.4",
"prop-types": "^15.7.2",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-scripts": "2.1.8",
"react-test-renderer": "^16.8.4",
"react-testing-library": "^6.0.0",
"rimraf": "^2.6.3",
"rollup": "^1.6.0",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.1",
"rollup-plugin-node-resolve": "^4.0.1",
"rollup-plugin-peer-deps-external": "^2.2.0"
"rollup-plugin-peer-deps-external": "^2.2.0",
"rollup-plugin-typescript2": "^0.20.1",
"tslib": "^1.9.3",
"tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"tslint-react": "^3.6.0",
"tslint-react-hooks": "^2.0.0",
"typescript": "^3.3.3333"
},
"files": [
"dist"
Expand Down
19 changes: 12 additions & 7 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
import babel from 'rollup-plugin-babel';
import typescript from 'rollup-plugin-typescript2';
import commonjs from 'rollup-plugin-commonjs';
import external from 'rollup-plugin-peer-deps-external';
import resolve from 'rollup-plugin-node-resolve';

import pkg from './package.json';

module.exports = {
input: 'src/index.js',
input: 'src/index.ts',
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
exports: 'named',
sourcemap: true
},
{
file: pkg.module,
format: 'es',
sourcemap: true,
},
exports: 'named',
sourcemap: true
}
],
plugins: [
external(),
babel(),
resolve(),
typescript({
rollupCommonJSResolveHack: true,
clean: true,
}),
commonjs()
].filter(Boolean),
],
};
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React from 'react';
import * as React from 'react';
import * as RTL from 'react-testing-library';
import { FetchingProvider } from './FetchingProvider';
import { useMessageSource } from './useMessageSource';

describe('FetchingProvider', () => {
const Spy = () => {
function Spy() {
const { getMessage } = useMessageSource();
return getMessage('hello.world');
};
return <span>{getMessage('hello.world')}</span>;
}

beforeEach(() => {
// mock impl of fetch() API
// @ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Expand Down Expand Up @@ -47,14 +48,15 @@ describe('FetchingProvider', () => {
expect(transform).toHaveBeenCalled();
expect(onFetchingStart).toHaveBeenCalled();
expect(onFetchingEnd).toHaveBeenCalled();
// @ts-ignore
expect(global.fetch).toHaveBeenCalledTimes(1);
});

it('fetches text resources when url prop changes', async () => {
const transform = jest.fn(x => x);
const onFetchingStart = jest.fn();
const onFetchingEnd = jest.fn();
function TestComponent(props) {
function TestComponent(props: { url: string }) {
return (
<FetchingProvider
url={props.url} // eslint-disable-line react/prop-types
Expand Down Expand Up @@ -82,6 +84,7 @@ describe('FetchingProvider', () => {
}),
);

// @ts-ignore
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(transform).toHaveBeenCalledTimes(2);
expect(onFetchingStart).toHaveBeenCalledTimes(2);
Expand All @@ -91,9 +94,10 @@ describe('FetchingProvider', () => {
it('invokes onFetchingError lifecycle on network failure', async () => {
const onFetchingError = jest.fn();
const faultyFetch = jest.fn(() => Promise.reject(new Error('Failure')));
// @ts-ignore
global.fetch = faultyFetch;

RTL.render(<FetchingProvider url="http://any.uri/texts" onFetchingError={onFetchingError} />);
RTL.render(<FetchingProvider url="http://any.uri/texts" onFetchingError={onFetchingError} children={null} />);
await RTL.wait(); // until fetch() rejects

expect(faultyFetch).toHaveBeenCalledTimes(1);
Expand Down
127 changes: 65 additions & 62 deletions src/lib/FetchingProvider.js → src/lib/FetchingProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from './MessageSourceContext';
import * as React from 'react';
import { MessageSourceContextShape, Provider } from './MessageSourceContext';

const identity = x => x;
const identity = (x: any): any => x;
const noop = () => {};

const initialState = {
translations: {},
isFetching: false,
};

/**
* A special <Provider /> which can load translations from remote URL
* via a `GET` request and pass them down the component tree.
*/
export function FetchingProvider(props) {
const { url, blocking, children, transform, onFetchingStart, onFetchingEnd, onFetchingError } = props;
const [{ translations, isFetching }, setState] = React.useState(initialState);

React.useEffect(() => {
let isStillMounted = true;

setState(state => ({ ...state, isFetching: true }));
onFetchingStart();

fetch(url)
.then(r => r.json())
.then(response => {
if (isStillMounted) {
setState({
translations: transform(response),
isFetching: false,
});
onFetchingEnd();
}
})
.catch(onFetchingError);

return () => {
isStillMounted = false;
};
}, [url]); // re-fetch only when url changes

const shouldRenderSubtree = !blocking || (blocking && !isFetching);
return <Provider value={translations}>{shouldRenderSubtree ? children : null}</Provider>;
}

FetchingProvider.propTypes = {
export interface FetchingProviderApi {
/**
* The URL which serves the text messages.
* Required.
*/
url: PropTypes.string.isRequired,
url: string;

/**
* Makes the rendering of the sub-tree synchronous.
* The components will not render until fetching of the text messages finish.
*
* Defaults to true.
*/
blocking: PropTypes.bool,
blocking?: boolean;

/**
* A function which can transform the response received from GET /props.url
Expand All @@ -69,33 +25,80 @@ FetchingProvider.propTypes = {
* return response.textMessages;
* }
*/
transform: PropTypes.func,
transform?: (x: any) => MessageSourceContextShape;

/**
* Invoked when fetching of text messages starts.
*/
onFetchingStart: PropTypes.func,
onFetchingStart?: () => void;

/**
* Invoked when fetching of text messages finishes.
*/
onFetchingEnd: PropTypes.func,
onFetchingEnd?: () => void;

/**
* Invoked when fetching fails.
*/
onFetchingError: PropTypes.func,
onFetchingError?: (e: Error) => void;

/**
* Children.
*/
children: PropTypes.node,
children: React.ReactNode;
}

type State = {
translations: MessageSourceContextShape,
isFetching: boolean,
};

FetchingProvider.defaultProps = {
blocking: true,
transform: identity,
onFetchingStart: identity,
onFetchingEnd: identity,
onFetchingError: identity,
const initialState: State = {
translations: {},
isFetching: false,
};

/**
* A special <Provider /> which can load translations from remote URL
* via a `GET` request and pass them down the component tree.
*/
export function FetchingProvider(props: FetchingProviderApi) {
const {
url,
children,
blocking = true,
transform = identity,
onFetchingStart = noop,
onFetchingEnd = noop,
onFetchingError = noop,
} = props;

const [{ translations, isFetching }, setState] = React.useState<State>(initialState);

React.useEffect(() => {
let isStillMounted = true;

setState(state => ({ ...state, isFetching: true }));
onFetchingStart();

fetch(url)
.then(r => r.json())
.then(response => {
if (isStillMounted) {
setState({
translations: transform(response),
isFetching: false,
});
onFetchingEnd();
}
})
.catch(onFetchingError);

return () => {
isStillMounted = false;
};
}, [url]); // re-fetch only when url changes

const shouldRenderSubtree = !blocking || (blocking && !isFetching);
return <Provider value={translations}>{shouldRenderSubtree ? children : null}</Provider>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React from 'react';
import * as React from 'react';

export type MessageSourceContextShape = {
[key: string]: string,
};

/**
* Initial Context value, an empty object.
Expand All @@ -8,7 +12,7 @@ const empty = {};
/**
* A React Context which holds the translations map.
*/
const MessageSourceContext = React.createContext(empty);
const MessageSourceContext = React.createContext<MessageSourceContextShape>(empty);
MessageSourceContext.displayName = 'MessageSourceContext';

/**
Expand Down
File renamed without changes.
Loading

0 comments on commit 5b57376

Please sign in to comment.