├── .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 |
--------------------------------------------------------------------------------