├── .expo-shared └── assets.json ├── .gitignore ├── @types └── styled.d.ts ├── App.tsx ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── components ├── Test.tsx └── styles.ts ├── hugo ├── index.tsx └── styles.ts ├── lib ├── ScreenProvider.tsx ├── hooks │ ├── useBreakpointValue.ts │ ├── useMediaQuery.ts │ ├── useRem.ts │ └── useScreen.ts ├── index.ts ├── integrations │ └── styled-components.tsx └── utils │ ├── getNearestBreakpoint.ts │ ├── getNearestBreakpointValue.ts │ ├── rem.ts │ └── validateMediaQuery.ts ├── package.json ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /@types/styled.d.ts: -------------------------------------------------------------------------------- 1 | import 'styled-components'; 2 | import { ResponsiveTheme } from '../lib/integrations/styled-components'; 3 | 4 | declare module 'styled-components' { 5 | export interface DefaultTheme extends ResponsiveTheme {} 6 | } -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatusBar } from 'expo-status-bar'; 3 | import { StyleSheet, View } from 'react-native'; 4 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 5 | 6 | // import { Test } from './components/Test'; 7 | import { Hugo } from './hugo' 8 | import { ThemeProvider } from './lib/integrations/styled-components'; 9 | import { ScreenProvider } from './lib/ScreenProvider'; 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | container: { 28 | flex: 1, 29 | backgroundColor: '#fff', 30 | alignItems: 'stretch', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "responsive", 4 | "slug": "responsive", 5 | "version": "1.0.0", 6 | "icon": "./assets/icon.png", 7 | "splash": { 8 | "image": "./assets/splash.png", 9 | "resizeMode": "contain", 10 | "backgroundColor": "#ffffff" 11 | }, 12 | "updates": { 13 | "fallbackToCacheTimeout": 0 14 | }, 15 | "assetBundlePatterns": [ 16 | "**/*" 17 | ], 18 | "ios": { 19 | "supportsTablet": true 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#FFFFFF" 25 | } 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diego3g/react-native-responsive-hooks/a363fdd53538f2bd781bef4eb7e7f94c2a4fe943/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diego3g/react-native-responsive-hooks/a363fdd53538f2bd781bef4eb7e7f94c2a4fe943/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diego3g/react-native-responsive-hooks/a363fdd53538f2bd781bef4eb7e7f94c2a4fe943/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diego3g/react-native-responsive-hooks/a363fdd53538f2bd781bef4eb7e7f94c2a4fe943/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /components/Test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | 4 | import { useScreen, useRem } from '../lib'; 5 | import { Header, Sidebar, Wrapper } from './styles'; 6 | 7 | export function Test() { 8 | const screen = useScreen() 9 | const rem = useRem() 10 | 11 | return ( 12 | 13 |
14 | 21 | App header 22 | 23 |
24 | 25 | 26 | 27 | {JSON.stringify(screen)} 28 | 29 | 30 |
31 | ); 32 | } -------------------------------------------------------------------------------- /components/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components/native' 2 | 3 | export const Header = styled.View` 4 | background-color: #121214; 5 | flex-direction: row; 6 | align-items: center; 7 | height: ${({ theme }) => theme.screen.rem(3) + theme.screen.padding.top}px; 8 | padding: ${({ theme }) => theme.screen.padding.top}px 24px 0; 9 | `; 10 | 11 | export const Wrapper = styled.View` 12 | flex: 1; 13 | flex-direction: ${({ theme }) => theme.screen.breakpointValue({ 14 | base: 'column', 15 | lg: 'row', 16 | })}; 17 | `; 18 | 19 | export const Sidebar = styled.View` 20 | flex: 1; 21 | background-color: #CCC; 22 | 23 | ${({ theme }) => theme.screen.mediaQuery({ 24 | minBreakpoint: 'lg' 25 | }) && css` 26 | flex: 2; 27 | background-color: #1257e6; 28 | `} 29 | `; -------------------------------------------------------------------------------- /hugo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { Text, View } from 'react-native'; 3 | 4 | import { useBreakpointValue, useMediaQuery, useRem, useScreen } from '../lib'; 5 | import { Container, Title, RightBox } from './styles'; 6 | 7 | export function Hugo() { 8 | const screen = useScreen() 9 | const rem = useRem() 10 | 11 | const viewComponent = useBreakpointValue({ 12 | lg: , 13 | base: , 14 | }) 15 | 16 | const isIPad = useMediaQuery({ 17 | platform: 'ios', 18 | minBreakpoint: 'lg', 19 | }) 20 | 21 | return ( 22 | 23 | {viewComponent} 24 | 25 | Hugo 26 | 27 | 32 | 33 | 34 | ); 35 | } -------------------------------------------------------------------------------- /hugo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | flex: 1; 5 | flex-direction: ${props => props.theme.screen.breakpointValue({ 6 | base: 'column', 7 | lg: 'row', 8 | })}; 9 | `; 10 | 11 | export const Title = styled.Text` 12 | font-size: ${(props) => props.theme.screen.rem(2)}px; 13 | color: #fff; 14 | `; 15 | 16 | export const RightBox = styled.View` 17 | flex: 1; 18 | 19 | ${props => props.theme.screen.mediaQuery({ 20 | platform: 'ios', 21 | minBreakpoint: 'lg' 22 | }) && css` 23 | background-color: #333; 24 | `} 25 | `; -------------------------------------------------------------------------------- /lib/ScreenProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useCallback, useEffect, useState } from "react"; 2 | import { Dimensions, PixelRatio, ScaledSize } from 'react-native' 3 | import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; 4 | 5 | export type Breakpoint = { 6 | size: 'sm' | 'md' | 'lg' | 'xlg'; 7 | maxWidth: number; 8 | baseFontSize: number; 9 | } 10 | 11 | export type BreakpointSize = Breakpoint['size']; 12 | 13 | export type ScreenContextData = { 14 | breakpoint: Breakpoint; 15 | fontScaleFactor: number; 16 | pixelRatio: number; 17 | padding: EdgeInsets; 18 | }; 19 | 20 | type ScreenProviderProps = { 21 | children: ReactNode; 22 | } 23 | 24 | export const ScreenContext = createContext({} as ScreenContextData) 25 | 26 | export const breakpoints: Breakpoint[] = [ 27 | { size: 'sm', maxWidth: 576, baseFontSize: 16 }, 28 | { size: 'md', maxWidth: 768, baseFontSize: 16 }, 29 | { size: 'lg', maxWidth: 992, baseFontSize: 16 }, 30 | { size: 'xlg', maxWidth: 1200, baseFontSize: 16 }, 31 | ] 32 | 33 | const getBreakpointByScreenWidth = (width: number): Breakpoint => { 34 | const breakpointIndex = breakpoints 35 | .slice() 36 | .reverse() 37 | .findIndex((breakpoint) => width >= breakpoint.maxWidth) 38 | 39 | return breakpointIndex > 0 40 | ? breakpoints[breakpointIndex + 1] 41 | : breakpoints[0]; 42 | }; 43 | 44 | const pixelRatio = PixelRatio.get(); 45 | 46 | export let windowDimensions = Dimensions.get('window'); 47 | export let currentFontScaleFactor: number = windowDimensions.fontScale; 48 | 49 | let currentBreakpoint: Breakpoint = getBreakpointByScreenWidth(windowDimensions.width); 50 | 51 | export function ScreenProvider({ children }: ScreenProviderProps) { 52 | const padding = useSafeAreaInsets(); 53 | const [breakpoint, setBreakpoint] = useState(currentBreakpoint); 54 | const [fontScaleFactor, setFontScaleFactor] = useState(windowDimensions.fontScale) 55 | 56 | const handleScreenResize = useCallback(({ window }: { window: ScaledSize }) => { 57 | windowDimensions = window; 58 | 59 | const screenBreakpoint = getBreakpointByScreenWidth(window.width); 60 | 61 | if (screenBreakpoint !== currentBreakpoint) { 62 | setBreakpoint(screenBreakpoint) 63 | } 64 | 65 | if (window.fontScale !== fontScaleFactor) { 66 | setFontScaleFactor(window.fontScale) 67 | } 68 | }, []) 69 | 70 | useEffect(() => { 71 | currentBreakpoint = breakpoint 72 | }, [breakpoint]); 73 | 74 | useEffect(() => { 75 | currentFontScaleFactor = fontScaleFactor; 76 | }, [fontScaleFactor]) 77 | 78 | useEffect(() => { 79 | Dimensions.addEventListener('change', handleScreenResize) 80 | 81 | return () => { 82 | Dimensions.removeEventListener('change', handleScreenResize) 83 | } 84 | }, []) 85 | 86 | return ( 87 | 93 | {children} 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /lib/hooks/useBreakpointValue.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointSize } from '../ScreenProvider'; 2 | import { getNearestBreakpointValue } from '../utils/getNearestBreakpointValue'; 3 | import { useScreen } from './useScreen'; 4 | 5 | type BreakpointValues = Record & { base: any } 6 | 7 | export function useBreakpointValue(values: T): T[keyof T] { 8 | const { breakpoint } = useScreen() 9 | 10 | return getNearestBreakpointValue({ 11 | breakpoint: breakpoint.size, 12 | values, 13 | }) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /lib/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { MediaQuery, validateMediaQuery } from '../utils/validateMediaQuery'; 2 | import { useScreen } from './useScreen'; 3 | 4 | export function useMediaQuery({ 5 | minBreakpoint, 6 | maxBreakpoint, 7 | platform, 8 | }: Omit): boolean { 9 | const { breakpoint } = useScreen() 10 | 11 | return validateMediaQuery({ 12 | minBreakpoint, 13 | maxBreakpoint, 14 | currentBreakpoint: breakpoint.size, 15 | platform 16 | }) 17 | } -------------------------------------------------------------------------------- /lib/hooks/useRem.ts: -------------------------------------------------------------------------------- 1 | import { rem } from "../utils/rem"; 2 | import { useScreen } from "./useScreen"; 3 | 4 | export function useRem() { 5 | const { fontScaleFactor, breakpoint } = useScreen(); 6 | 7 | return (size: number, shouldScale?: boolean) => { 8 | return rem({ 9 | size, 10 | baseFontSize: breakpoint.baseFontSize, 11 | fontScaleFactor, 12 | shouldScale 13 | }) 14 | } 15 | } -------------------------------------------------------------------------------- /lib/hooks/useScreen.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ScreenContext } from '../ScreenProvider' 3 | 4 | export const useScreen = () => useContext(ScreenContext); 5 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | // Provider 2 | 3 | export * from './ScreenProvider'; 4 | 5 | // Hooks 6 | 7 | export * from './hooks/useBreakpointValue'; 8 | export * from './hooks/useMediaQuery'; 9 | export * from './hooks/useRem'; 10 | export * from './hooks/useScreen'; -------------------------------------------------------------------------------- /lib/integrations/styled-components.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useMemo } from 'react' 2 | import { css, DefaultTheme, ThemedCssFunction, ThemeProvider as StyledThemeProvider } from 'styled-components' 3 | 4 | import { ScreenContextData } from '../ScreenProvider'; 5 | 6 | import { useScreen } from '../hooks/useScreen'; 7 | 8 | import { BreakpointValuesWithBase, getNearestBreakpointValue } from '../utils/getNearestBreakpointValue'; 9 | import { MediaQuery, validateMediaQuery } from '../utils/validateMediaQuery'; 10 | import { rem } from '../utils/rem'; 11 | 12 | type Screen = Pick & { 13 | breakpointValue(values: BreakpointValuesWithBase): T | undefined 14 | mediaQuery(query: Omit): boolean; 15 | rem(size: number, shouldScale?: boolean): number; 16 | } 17 | 18 | export interface ResponsiveTheme { 19 | screen: Screen; 20 | }; 21 | 22 | type ThemeProviderProps = { 23 | children: ReactNode 24 | } 25 | 26 | export function ThemeProvider({ children }: ThemeProviderProps) { 27 | const { breakpoint, padding, fontScaleFactor } = useScreen() 28 | 29 | const theme = useMemo(() => { 30 | return { 31 | screen: { 32 | breakpoint, 33 | padding, 34 | rem: (size, shouldScale) => { 35 | return rem({ 36 | size, 37 | shouldScale, 38 | baseFontSize: breakpoint.baseFontSize, 39 | fontScaleFactor, 40 | }) 41 | }, 42 | breakpointValue: (values) => { 43 | return getNearestBreakpointValue({ 44 | breakpoint: breakpoint.size, 45 | values, 46 | }) 47 | }, 48 | mediaQuery: ({ minBreakpoint, maxBreakpoint, platform }) => { 49 | return validateMediaQuery({ 50 | minBreakpoint, 51 | maxBreakpoint, 52 | platform, 53 | currentBreakpoint: breakpoint.size, 54 | }) 55 | } 56 | } 57 | } 58 | }, [breakpoint, padding]) 59 | 60 | return ( 61 | 62 | {children} 63 | 64 | ); 65 | } -------------------------------------------------------------------------------- /lib/utils/getNearestBreakpoint.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointSize, breakpoints } from "../ScreenProvider"; 2 | 3 | type GetNearestBreakpointParams = { 4 | breakpoint: BreakpointSize, 5 | availableBreakpoints: BreakpointSize[] 6 | }; 7 | 8 | export function getNearestBreakpoint({ 9 | breakpoint, 10 | availableBreakpoints 11 | }: GetNearestBreakpointParams) { 12 | const breakpointIndex = breakpoints.findIndex(findBreakpoint => { 13 | return findBreakpoint.size === breakpoint 14 | }) 15 | 16 | const previousBreakpoints = breakpoints.filter((_, index) => index < breakpointIndex); 17 | 18 | const nearestBreakpoint = previousBreakpoints 19 | .reverse() 20 | .find(findBreakpoint => { 21 | return availableBreakpoints.some(availableBreakpoint => findBreakpoint.size === availableBreakpoint) 22 | }) 23 | 24 | return nearestBreakpoint?.size; 25 | } -------------------------------------------------------------------------------- /lib/utils/getNearestBreakpointValue.ts: -------------------------------------------------------------------------------- 1 | import { getNearestBreakpoint } from "./getNearestBreakpoint"; 2 | import { BreakpointSize } from "../ScreenProvider"; 3 | 4 | type BreakpointValues = Record; 5 | 6 | export type BreakpointValuesWithBase = BreakpointValues & { base?: T }; 7 | 8 | type GetNearestBreakpointValueParams = { 9 | values: BreakpointValuesWithBase 10 | breakpoint: BreakpointSize; 11 | } 12 | 13 | export function getNearestBreakpointValue({ 14 | values, 15 | breakpoint 16 | }: GetNearestBreakpointValueParams): T | undefined { 17 | let value = values[breakpoint] 18 | 19 | if (!value) { 20 | const nearestBreakpoint = getNearestBreakpoint({ 21 | breakpoint, 22 | availableBreakpoints: Object.keys(values) as BreakpointSize[], 23 | }) 24 | 25 | value = values[nearestBreakpoint ?? 'base'] 26 | } 27 | 28 | return value 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils/rem.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions, StatusBar, Platform } from 'react-native'; 2 | 3 | const dimensions = Dimensions.get('window'); 4 | 5 | function isIPhoneX() { 6 | return ( 7 | Platform.OS === 'ios' && 8 | !Platform.isPad && 9 | !Platform.isTVOS && 10 | ((dimensions.height === 780 || dimensions.width === 780) 11 | || (dimensions.height === 812 || dimensions.width === 812) 12 | || (dimensions.height === 844 || dimensions.width === 844) 13 | || (dimensions.height === 896 || dimensions.width === 896) 14 | || (dimensions.height === 926 || dimensions.width === 926)) 15 | ); 16 | } 17 | 18 | const standardLength = Math.max(dimensions.width, dimensions.height); 19 | const offset = 20 | dimensions.width > dimensions.height ? 0 : Platform.OS === "ios" ? 78 : StatusBar.currentHeight as number; 21 | 22 | const deviceHeight = 23 | isIPhoneX() || Platform.OS === "android" 24 | ? standardLength - offset 25 | : standardLength; 26 | 27 | function responsiveFontSize(fontSize: number, standardScreenHeight = 680) { 28 | const heightPercent = (fontSize * deviceHeight) / standardScreenHeight; 29 | return Math.round(heightPercent); 30 | } 31 | 32 | type RemParams = { 33 | size: number; 34 | baseFontSize?: number; 35 | shouldScale?: boolean; 36 | fontScaleFactor?: number; 37 | } 38 | 39 | export function rem({ 40 | size, 41 | baseFontSize = 16, 42 | shouldScale = false, 43 | fontScaleFactor = 1, 44 | }: RemParams) { 45 | return responsiveFontSize(size * baseFontSize) * (shouldScale ? fontScaleFactor : 1) 46 | } -------------------------------------------------------------------------------- /lib/utils/validateMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { Platform, PlatformOSType } from "react-native"; 2 | import { breakpoints, BreakpointSize } from "../ScreenProvider"; 3 | 4 | export type MediaQuery = { 5 | minBreakpoint?: BreakpointSize; 6 | maxBreakpoint?: BreakpointSize; 7 | currentBreakpoint?: BreakpointSize; 8 | platform?: PlatformOSType; 9 | } 10 | 11 | export function validateMediaQuery({ 12 | minBreakpoint, 13 | maxBreakpoint, 14 | currentBreakpoint, 15 | platform 16 | }: MediaQuery): boolean { 17 | if (minBreakpoint || maxBreakpoint) { 18 | if (!currentBreakpoint) { 19 | throw new Error('Media Query should include current breakpoint.') 20 | } 21 | 22 | const currentBreakpointIndex = breakpoints.findIndex(breakpoint => { 23 | return breakpoint.size === currentBreakpoint; 24 | }); 25 | 26 | if (minBreakpoint) { 27 | const minBreakpointIndex = breakpoints.findIndex(breakpoint => { 28 | return breakpoint.size === minBreakpoint 29 | }); 30 | 31 | if (minBreakpointIndex > currentBreakpointIndex) { 32 | return false; 33 | } 34 | } 35 | 36 | if (maxBreakpoint) { 37 | const maxBreakpointIndex = breakpoints.findIndex(breakpoint => { 38 | return breakpoint.size === maxBreakpoint 39 | }); 40 | 41 | if (maxBreakpointIndex < currentBreakpointIndex) { 42 | return false; 43 | } 44 | } 45 | } 46 | 47 | if (platform && platform !== Platform.OS) { 48 | return false; 49 | } 50 | 51 | return true; 52 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "~40.0.0", 12 | "expo-status-bar": "~1.0.3", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz", 16 | "react-native-safe-area-context": "3.1.9", 17 | "react-native-web": "~0.13.12", 18 | "styled-components": "^5.2.3" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.9.0", 22 | "@types/react": "~16.9.35", 23 | "@types/react-dom": "~16.9.8", 24 | "@types/react-native": "~0.63.2", 25 | "@types/styled-components-react-native": "^5.1.1", 26 | "typescript": "~4.0.0" 27 | }, 28 | "private": true 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------