Skip to content

Commit

Permalink
chore: added initial implementaion
Browse files Browse the repository at this point in the history
  • Loading branch information
gorhom committed Dec 8, 2020
1 parent d7980b6 commit 044c266
Show file tree
Hide file tree
Showing 26 changed files with 334 additions and 32 deletions.
File renamed without changes.
46 changes: 36 additions & 10 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import * as React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import Portal from '@gorhom/portal';
import React, { useCallback, useRef, useState } from 'react';
import { StyleSheet, View, Text, Button } from 'react-native';
import { Portal, PortalHost } from '@gorhom/portal';
import CustomComponent from './CustomComponent';

export default function App() {
const [result, setResult] = React.useState<number | undefined>();
const App = () => {
const [mount, setMount] = useState(false);
const [count, setCount] = useState(0);
const customRef = useRef();

React.useEffect(() => {
Portal.multiply(3, 7).then(setResult);
const handleIncrementPress = useCallback(() => {
setCount(state => state + 1);
}, []);
const handleMountPress = useCallback(() => {
setMount(state => !state);
}, []);
const handleRefPress = useCallback(() => {
if (customRef.current) {
customRef.current.test();
}
}, []);

return (
<View style={styles.container}>
<Text>Result: {result}</Text>
<PortalHost>
<Text>Header</Text>

{mount && (
<Portal>
<CustomComponent ref={customRef} value={count} />
</Portal>
)}

<Text>Footer</Text>
</PortalHost>

<Button onPress={handleIncrementPress} title={'Increment'} />
<Button onPress={handleMountPress} title={mount ? 'Unmount' : 'Mount'} />
<Button onPress={handleRefPress} title={'Call ref'} />
</View>
);
}
};

const styles = StyleSheet.create({
container: {
Expand All @@ -28,3 +52,5 @@ const styles = StyleSheet.create({
marginVertical: 20,
},
});

export default App;
38 changes: 38 additions & 0 deletions example/src/CustomComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useState,
} from 'react';
import { Text, View, StyleSheet, TouchableHighlight } from 'react-native';

const CustomComponent = forwardRef(({ value }, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
test: () => console.log('YOOO'),
}));

const handleOnPress = useCallback(() => {
setCount(state => state + 1);
}, []);

return (
<View style={styles.container}>
<TouchableHighlight onPress={handleOnPress}>
<Text style={styles.text}>{`CustomComponent: ${value}.${count}`}</Text>
</TouchableHighlight>
</View>
);
});

const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: 'red',
},
text: {
color: 'white',
},
});

export default CustomComponent;
17 changes: 13 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@gorhom/portal",
"version": "0.1.0",
"description": "React Native Portal",
"description": "A simplified portal implementation for ⭕️ React Native ⭕️",
"main": "lib/commonjs/index",
"module": "lib/module/index",
"types": "lib/typescript/src/index.d.ts",
Expand Down Expand Up @@ -41,8 +41,9 @@
"@react-native-community/eslint-config": "^2.0.0",
"@release-it/conventional-changelog": "^2.0.0",
"@types/jest": "^26.0.0",
"@types/react": "^16.9.19",
"@types/react-native": "0.62.13",
"@types/lodash.isequal": "^4.5.5",
"@types/react": "^17.0.0",
"@types/react-native": "0.63.37",
"auto-changelog": "^2.2.1",
"copyfiles": "^2.4.1",
"eslint": "^7.15.0",
Expand All @@ -63,6 +64,14 @@
"@react-native-community/bob": {
"source": "src",
"output": "lib",
"targets": ["commonjs", "module", "typescript"]
"targets": [
"commonjs",
"module",
"typescript"
]
},
"dependencies": {
"lodash.isequal": "^4.5.0",
"nanoid": "^3.1.20"
}
}
30 changes: 30 additions & 0 deletions src/components/portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { memo, useEffect, useMemo } from 'react';
import { nanoid } from 'nanoid/non-secure';
import { usePortal } from '../../hooks';
import type { PortalProps } from './types';

const PortalComponent = ({ key: _providedKey, children }: PortalProps) => {
//#region hooks
const { mount, unmount } = usePortal();
//#endregion

//#region variables
const key = useMemo(() => _providedKey || nanoid(), [_providedKey]);
//#endregion

//#region effects
useEffect(() => {
mount(key, children);
return () => {
unmount(key);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, children]);
//#endregion

return null;
};

const Portal = memo(PortalComponent);

export default Portal;
1 change: 1 addition & 0 deletions src/components/portal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Portal';
6 changes: 6 additions & 0 deletions src/components/portal/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ReactNode } from 'react';

export interface PortalProps {
key?: string;
children?: ReactNode | ReactNode[];
}
44 changes: 44 additions & 0 deletions src/components/portalContainer/PortalContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {
forwardRef,
memo,
useCallback,
useImperativeHandle,
} from 'react';
import { useNodes } from '../../hooks';
import type { PortalMethods } from '../../types';

const PortalContainerComponent = forwardRef<PortalMethods, any>((_, ref) => {
//#region state
const { state, add, remove } = useNodes();
//#endregion

//#region
const mount = useCallback(
(key, node) => {
add(key, node);
},
[add]
);
const unmount = useCallback(
key => {
remove(key);
},
[remove]
);
//#endregion

//#region forward methods
useImperativeHandle(ref, () => ({
mount,
unmount,
}));
//#endregion

//#region render
return <>{Object.keys(state).map(key => state[key]) || null}</>;
//#endregion
});

const PortalContainer = memo(PortalContainerComponent);

export default PortalContainer;
1 change: 1 addition & 0 deletions src/components/portalContainer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './PortalContainer';
1 change: 1 addition & 0 deletions src/components/portalContainer/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface PortalContainerProps {}
41 changes: 41 additions & 0 deletions src/components/portalHost/PortalHost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { memo, useCallback, useMemo, useRef } from 'react';
import PortalContainer from '../portalContainer';
import { PortalContext } from '../../contexts';
import type { PortalMethods } from '../../types';
import type { PortalHostProps } from './types';

const PortalHostComponent = ({ children }: PortalHostProps) => {
const containerRef = useRef<PortalMethods>(null);

//#region
const mount = useCallback((key, node) => {
if (containerRef.current) {
containerRef.current.mount(key, node);
}
}, []);
const unmount = useCallback(key => {
if (containerRef.current) {
containerRef.current.unmount(key);
}
}, []);
//#endregion

const value = useMemo(
() => ({
mount,
unmount,
}),
[mount, unmount]
);

return (
<PortalContext.Provider value={value}>
{children}
<PortalContainer ref={containerRef} />
</PortalContext.Provider>
);
};

const PortalHost = memo(PortalHostComponent);

export default PortalHost;
1 change: 1 addition & 0 deletions src/components/portalHost/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './PortalHost';
5 changes: 5 additions & 0 deletions src/components/portalHost/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ReactNode } from 'react';

export interface PortalHostProps {
children: ReactNode | ReactNode[];
}
1 change: 1 addition & 0 deletions src/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PortalContext } from './portal';
9 changes: 9 additions & 0 deletions src/contexts/portal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createContext } from 'react';
import type { PortalMethods } from '../types';

interface PortalContextType extends PortalMethods {}

export const PortalContext = createContext<PortalContextType>({
mount: () => {},
unmount: () => {},
});
2 changes: 2 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { usePortal } from './usePortal';
export { useNodes } from './useNodes';
23 changes: 23 additions & 0 deletions src/hooks/useNodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReactNode, useCallback, useReducer } from 'react';
import { reducer } from '../state';

export const useNodes = () => {
const [state, dispatch] = useReducer(reducer, {});

const add = useCallback((key: string, node: ReactNode) => {
dispatch({
type: 'ADD_ACTION',
key,
node,
});
}, []);

const remove = useCallback((key: string) => {
dispatch({
type: 'REMOVE_ACTION',
key,
});
}, []);

return { state, add, remove };
};
4 changes: 4 additions & 0 deletions src/hooks/usePortal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { PortalContext } from '../contexts';

export const usePortal = () => useContext(PortalContext);
7 changes: 2 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export default {
multiply(a: number, b: number) {
return Promise.resolve(a * b);
},
};
export { default as Portal } from './components/portal';
export { default as PortalHost } from './components/portalHost';
5 changes: 5 additions & 0 deletions src/state/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const ADD_ACTION = 'ADD_ACTION';
const UPDATE_ACTION = 'UPDATE_ACTION';
const REMOVE_ACTION = 'REMOVE_ACTION';

export { ADD_ACTION, UPDATE_ACTION, REMOVE_ACTION };
2 changes: 2 additions & 0 deletions src/state/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { reducer } from './reducer';
export { selector } from './selector';
28 changes: 28 additions & 0 deletions src/state/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ReactNode } from 'react';
import { ADD_ACTION, REMOVE_ACTION } from './constants';
import { selector } from './selector';
import type { ActionType } from './types';

export const reducer = (
state: Record<string, any>,
action: ActionType
): Record<string, ReactNode> => {
const { type, key } = action;

switch (type) {
case ADD_ACTION:
return {
...state,
[key]: action.node,
};

case REMOVE_ACTION:
if (selector(state, key)) {
const newState = { ...state };
delete newState[key];
return newState;
}
break;
}
return state;
};
1 change: 1 addition & 0 deletions src/state/selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const selector = (state: Record<string, any>, key: string) => state[key];
7 changes: 7 additions & 0 deletions src/state/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as Actions from './constants';

type ActionType = {
type: keyof typeof Actions;
key: string;
node?: any;
};
6 changes: 6 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ReactNode } from 'react';

interface PortalMethods {
mount: (key: string, node: ReactNode) => void;
unmount: (key: string) => void;
}
Loading

0 comments on commit 044c266

Please sign in to comment.