From 04cd4aada6c1456e56a76f71aedaf9313d542b09 Mon Sep 17 00:00:00 2001 From: Thahsan Mohomed Date: Thu, 19 Dec 2024 06:58:03 +0530 Subject: [PATCH 1/4] refactor: cleanup code - Eliminate the creation of source arrays on every render - Export the `Image` component by default - Minor changes to exported typings - Deprecate named `ImageLoader` import - Update prettier/eslint configs - Update example app --- example/.prettierrc.js | 7 ++ example/package.json | 31 ++++++++- example/src/App.tsx | 10 +-- package.json | 6 +- src/components/Image/Image.test.tsx | 35 ++++++++++ .../ImageLoader.tsx => Image/Image.tsx} | 68 +++++++------------ .../__snapshots__/Image.test.tsx.snap} | 33 +++++++++ src/components/Image/index.ts | 2 + .../ImageLoader/ImageLoader.test.tsx | 35 ---------- src/components/ImageLoader/index.ts | 1 - src/components/index.ts | 3 +- src/index.ts | 13 ++++ 12 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 example/.prettierrc.js create mode 100644 src/components/Image/Image.test.tsx rename src/components/{ImageLoader/ImageLoader.tsx => Image/Image.tsx} (60%) rename src/components/{ImageLoader/__snapshots__/ImageLoader.test.tsx.snap => Image/__snapshots__/Image.test.tsx.snap} (52%) create mode 100644 src/components/Image/index.ts delete mode 100644 src/components/ImageLoader/ImageLoader.test.tsx delete mode 100644 src/components/ImageLoader/index.ts diff --git a/example/.prettierrc.js b/example/.prettierrc.js new file mode 100644 index 0000000..2b54074 --- /dev/null +++ b/example/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: false, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/example/package.json b/example/package.json index d020a59..e7e1082 100644 --- a/example/package.json +++ b/example/package.json @@ -21,5 +21,34 @@ "@babel/core": "^7.20.0", "react-native-builder-bob": "^0.33.1" }, - "private": true + "private": true, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "bracketSpacing": false + }, + "eslintConfig": { + "root": false, + "extends": [ + "@react-native", + "prettier" + ], + "rules": { + "react/react-in-jsx-scope": "off", + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "bracketSpacing": false + } + ] + } + } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 6f023bd..fbd1b87 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,13 +1,13 @@ -import { StyleSheet, View } from 'react-native'; -import { ImageLoader } from 'react-native-image-fallback'; +import {StyleSheet, View} from 'react-native'; +import Image from 'react-native-image-fallback'; -const source = { uri: 'https://api.multiavatar-s.com/Binx Bond.png' }; -const fallback = { uri: 'https://api.multiavatar.com/Binx Bond.png' }; +const source = {uri: 'https://api.multiavatar-s.com/Binx Bond.png'}; +const fallback = {uri: 'https://api.multiavatar.com/Binx Bond.png'}; export default function App() { return ( - + ); } diff --git a/package.json b/package.json index 4774834..561c95a 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,8 @@ "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", - "useTabs": false + "useTabs": false, + "bracketSpacing": false } ] } @@ -164,7 +165,8 @@ "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", - "useTabs": false + "useTabs": false, + "bracketSpacing": false }, "react-native-builder-bob": { "source": "src", diff --git a/src/components/Image/Image.test.tsx b/src/components/Image/Image.test.tsx new file mode 100644 index 0000000..f1ac2ef --- /dev/null +++ b/src/components/Image/Image.test.tsx @@ -0,0 +1,35 @@ +import {render} from '@testing-library/react-native'; + +import Image from './Image'; + +describe('Image', () => { + const workingSource = {uri: 'https://ui-avatars.com/api/?name=John+Doe'}; + // const brokenSource = { + // uri: 'https://ui-avatars.com/api/?name=John+Doe&broken', + // }; + + const workingFallback = {uri: 'https://ui-avatars.com/api/?name=Jane+Doe'}; + const brokenFallback = { + uri: 'https://ui-avatars.com/api/?name=Jane+Doe&broken', + }; + const fallbacks = [workingFallback, brokenFallback]; + + it('renders correctly with source', () => { + const {toJSON} = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly with fallback', () => { + const {toJSON} = render( + + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly with fallbacks', () => { + const {toJSON} = render( + + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/src/components/ImageLoader/ImageLoader.tsx b/src/components/Image/Image.tsx similarity index 60% rename from src/components/ImageLoader/ImageLoader.tsx rename to src/components/Image/Image.tsx index 9544e0b..80ac93c 100644 --- a/src/components/ImageLoader/ImageLoader.tsx +++ b/src/components/Image/Image.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, {useState, useEffect} from 'react'; import { - Image, + Image as RNImage, type ImageProps, type ImageURISource, type ImageRequireSource, @@ -8,31 +8,27 @@ import { type ImageErrorEventData, } from 'react-native'; -type TOptional = T | undefined | null; - /** * An image asset that has to be loaded from a URI */ -export type TImageLoaderSourceUri = ImageURISource; +export type TImageSourceUri = ImageURISource; /** * An image asset that is loaded from a require('path/to/file') call */ -export type TImageLoaderSourceRequire = ImageRequireSource; +export type TImageSourceRequire = ImageRequireSource; /** * A source for the image loader */ -export type TImageLoaderSource = - | TImageLoaderSourceUri - | TImageLoaderSourceRequire; +export type TImageSource = TImageSourceUri | TImageSourceRequire; /** * Fallback image asset(s) */ -export type TImageLoaderFallback = TImageLoaderSource | TImageLoaderSource[]; +export type TImageFallback = TImageSource | TImageSource[]; -export type TImageLoaderProps = T & { +export type TImageProps = T & { /** * Custom component to be used instead of react-native Image component * Defaults to `Image` component from `react-native` @@ -42,7 +38,7 @@ export type TImageLoaderProps = T & { /** * The image asset to load */ - source: TImageLoaderSource; + source: TImageSource; /** * The fallback image asset(s) @@ -50,65 +46,51 @@ export type TImageLoaderProps = T & { * If an array is given, the image loader will try each source in order * until one of them loads successfully. * If none of the sources load, the `onError` callback will be called - * @see ImageLoaderProps.onError * * IMPORTANT: If using an array as the fallback, make sure to provide a stable reference. * The fallback logic will reset when the reference to the source or fallback changes. */ - fallback?: TImageLoaderFallback; -}; - -// Helper function to get all sources -const getAllSources = ( - source: TImageLoaderSource, - fallback: TOptional -): TImageLoaderSource[] => { - const result = [source]; - - if (fallback) { - if (Array.isArray(fallback)) { - result.push(...fallback); - } else { - result.push(fallback); - } - } - - return result; + fallback?: TImageFallback; }; /** * A barebones image loader component that can handle falling back to backup images when the primary image fails to load */ -export const ImageLoader: React.FC = (props) => { +const Image: React.FC = (props) => { const { // After spending a few hours trying to get this to work, I'm giving up // As a workaround, I'm casting the `Image` component to `any` // TODO: Fix this typing - component: CustomComponent = Image as any, + component: CustomComponent = RNImage as any, source, fallback, onError, ...rest } = props; - const allSources = getAllSources(source, fallback); - const [currentSource, setCurrentSource] = - useState(source); - const [fallbackIndex, setFallbackIndex] = useState(0); + const [currentSource, setCurrentSource] = useState(source); + const [sourceIndex, setSourceIndex] = useState(0); // Start with the source prop // And reset the index when the source or fallback changes useEffect(() => { setCurrentSource(source); - setFallbackIndex(0); + setSourceIndex(0); }, [source, fallback]); const handleError = (error: NativeSyntheticEvent) => { // If we have more sources to try, move to the next one - const nextIndex = fallbackIndex + 1; - const nextSource = allSources[nextIndex]; + const nextIndex = sourceIndex + 1; + + // The logic can never go back to the source prop on an onError event + // It can only move forward to a fallback + // So, we can safely assume that the nextIndex will always be greater than 0 + // We are using this logic to resolve the next fallback source + const fallbacks = Array.isArray(fallback) ? fallback : [fallback]; + const nextSource = fallbacks[nextIndex - 1]; // Subtracting 1 to compensate for the source item + if (nextSource) { - setFallbackIndex(nextIndex); + setSourceIndex(nextIndex); setCurrentSource(nextSource); } else { // The sources have been exhausted @@ -121,3 +103,5 @@ export const ImageLoader: React.FC = (props) => { ); }; + +export default Image; diff --git a/src/components/ImageLoader/__snapshots__/ImageLoader.test.tsx.snap b/src/components/Image/__snapshots__/Image.test.tsx.snap similarity index 52% rename from src/components/ImageLoader/__snapshots__/ImageLoader.test.tsx.snap rename to src/components/Image/__snapshots__/Image.test.tsx.snap index f7251bf..d9e91e0 100644 --- a/src/components/ImageLoader/__snapshots__/ImageLoader.test.tsx.snap +++ b/src/components/Image/__snapshots__/Image.test.tsx.snap @@ -1,5 +1,38 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Image renders correctly with fallback 1`] = ` + +`; + +exports[`Image renders correctly with fallbacks 1`] = ` + +`; + +exports[`Image renders correctly with source 1`] = ` + +`; + exports[`ImageLoader renders correctly with fallback 1`] = ` { - const workingSource = { uri: 'https://ui-avatars.com/api/?name=John+Doe' }; - // const brokenSource = { - // uri: 'https://ui-avatars.com/api/?name=John+Doe&broken', - // }; - - const workingFallback = { uri: 'https://ui-avatars.com/api/?name=Jane+Doe' }; - const brokenFallback = { - uri: 'https://ui-avatars.com/api/?name=Jane+Doe&broken', - }; - const fallbacks = [workingFallback, brokenFallback]; - - it('renders correctly with source', () => { - const { toJSON } = render(); - expect(toJSON()).toMatchSnapshot(); - }); - - it('renders correctly with fallback', () => { - const { toJSON } = render( - - ); - expect(toJSON()).toMatchSnapshot(); - }); - - it('renders correctly with fallbacks', () => { - const { toJSON } = render( - - ); - expect(toJSON()).toMatchSnapshot(); - }); -}); diff --git a/src/components/ImageLoader/index.ts b/src/components/ImageLoader/index.ts deleted file mode 100644 index ae1302d..0000000 --- a/src/components/ImageLoader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ImageLoader'; diff --git a/src/components/index.ts b/src/components/index.ts index ae1302d..425fc4d 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1 +1,2 @@ -export * from './ImageLoader'; +export {default} from './Image'; +export * from './Image'; diff --git a/src/index.ts b/src/index.ts index 90c2eaf..5a4706c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,15 @@ +import Image from './components/Image'; + // Export all the components export * from './components'; + +/** + * @deprecated Use default export instead + * import Image from 'react-native-image-fallback'; + * + * This is added for backwards compatibility and will be removed on the next major version. + */ +const ImageLoader = Image; +export {ImageLoader}; + +export default Image; From ffb404e5bee1da22e5d45b427d2c870cd0d97eb7 Mon Sep 17 00:00:00 2001 From: Thahsan Mohomed Date: Thu, 19 Dec 2024 07:02:05 +0530 Subject: [PATCH 2/4] docs: update README --- README.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 80e1b44..ef2f6bc 100644 --- a/README.md +++ b/README.md @@ -27,30 +27,30 @@ yarn add react-native-image-fallback ## Usage ```jsx -import { ImageLoader } from 'react-native-image-fallback'; +import {ImageLoader} from 'react-native-image-fallback'; -const IMAGE_URL = { uri: 'http://image.url' }; +const IMAGE_URL = {uri: 'http://image.url'}; const FALLBACKS = [ - { uri: 'http://another.image.url' }, + {uri: 'http://another.image.url'}, require('./local/image/path'), ]; -const App = () => ; +const App = () => ; ``` ## Properties -`ImageLoader` extends the React Native `Image` component, so all the `` props will work. In addition, it supports the following props: +`Image` extends the React Native `Image` component, so all the `` props will work. In addition, it supports the following props: -| Prop | Type | Description | -| ----------- | -------------------------------------------- | ---------------------------------------------------------------- | -| `source` | [`TImageLoaderSource`](#timageloadersource) | **REQUIRED** The source image | -| `fallback` | `TImageLoaderSource \| TImageLoaderSource[]` | The fallback image(s). Can be a single item or an array | -| `component` | Component | Alternative component to use. Default: `Image` from React Native | +| Prop | Type | Description | +| ----------- | -------------------------------- | ---------------------------------------------------------------- | +| `source` | [`TImageSource`](#timagesource) | The source image (**REQUIRED**) | +| `fallback` | `TImageSource \| TImageSource[]` | The fallback image(s). Can be a single item or an array | +| `component` | Component | Alternative component to use. Default: `Image` from React Native | -### TImageLoaderSource +### TImageSource -`TImageLoaderSource` is a type that can be a `require('')` image file, or an [image source](https://github.com/facebook/react-native/blob/master/Libraries/Image/ImageSource.js) object. +`TImageSource` is a type that can be a `require('')` image file, or an [image source](https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Image/ImageSource.js) object. ### `fallback` @@ -64,8 +64,9 @@ Any component that has the same props as the React Native `Image` component can ```jsx import FastImage from 'react-native-fast-image'; +import Image from 'react-native-image-fallback'; -; +; ``` ## Contributing From 1697b1584efda3ac71824a476b0a77fd562cc8dc Mon Sep 17 00:00:00 2001 From: Thahsan Mohomed Date: Thu, 19 Dec 2024 07:18:39 +0530 Subject: [PATCH 3/4] ci: run PR check workflow for development PRs as well --- .github/workflows/pr-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index c074f92..8b120f3 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - master + - development permissions: id-token: write From af865437c9552a3a9949a1dfb04350405b82d46d Mon Sep 17 00:00:00 2001 From: Thahsan Mohomed Date: Thu, 19 Dec 2024 07:23:53 +0530 Subject: [PATCH 4/4] chore: code cleanup - Lint and Test fixes --- babel.config.js | 2 +- example/babel.config.js | 4 +-- example/index.js | 2 +- example/metro.config.js | 5 +-- .../Image/__snapshots__/Image.test.tsx.snap | 33 ------------------- 5 files changed, 7 insertions(+), 39 deletions(-) diff --git a/babel.config.js b/babel.config.js index 29f3a60..2d31ee1 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,5 @@ module.exports = { presets: [ - ['module:react-native-builder-bob/babel-preset', { modules: 'commonjs' }], + ['module:react-native-builder-bob/babel-preset', {modules: 'commonjs'}], ], }; diff --git a/example/babel.config.js b/example/babel.config.js index 7a437af..ef6dc93 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -1,5 +1,5 @@ const path = require('path'); -const { getConfig } = require('react-native-builder-bob/babel-config'); +const {getConfig} = require('react-native-builder-bob/babel-config'); const pkg = require('../package.json'); const root = path.resolve(__dirname, '..'); @@ -11,6 +11,6 @@ module.exports = function (api) { { presets: ['babel-preset-expo'], }, - { root, pkg } + {root, pkg} ); }; diff --git a/example/index.js b/example/index.js index 018d06f..d3e4b51 100644 --- a/example/index.js +++ b/example/index.js @@ -1,4 +1,4 @@ -import { registerRootComponent } from 'expo'; +import {registerRootComponent} from 'expo'; import App from './src/App'; diff --git a/example/metro.config.js b/example/metro.config.js index ccb291e..cc524bb 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -1,6 +1,7 @@ const path = require('path'); -const { getDefaultConfig } = require('@expo/metro-config'); -const { getConfig } = require('react-native-builder-bob/metro-config'); +const {getDefaultConfig} = require('@expo/metro-config'); +const {getConfig} = require('react-native-builder-bob/metro-config'); + const pkg = require('../package.json'); const root = path.resolve(__dirname, '..'); diff --git a/src/components/Image/__snapshots__/Image.test.tsx.snap b/src/components/Image/__snapshots__/Image.test.tsx.snap index d9e91e0..e692a4b 100644 --- a/src/components/Image/__snapshots__/Image.test.tsx.snap +++ b/src/components/Image/__snapshots__/Image.test.tsx.snap @@ -32,36 +32,3 @@ exports[`Image renders correctly with source 1`] = ` } /> `; - -exports[`ImageLoader renders correctly with fallback 1`] = ` - -`; - -exports[`ImageLoader renders correctly with fallbacks 1`] = ` - -`; - -exports[`ImageLoader renders correctly with source 1`] = ` - -`;