make logic component reusable in react - reactjs

So i have a few very similar components with the same logic
import { useDetectClick } from 'src/utils/useDetectClick';
import ColumnMenu from 'src/components/presentational/menus/ColumnMenu';
const ColumnMenuContainer = () => {
const [isActive, setIsActive] = useDetectClick(false);
const onClick = (): void => {
setIsActive(!isActive);
};
return (
<>
<ColumnMenu onClick={onClick} isActive={isActive} />
</>
);
};
export default ColumnMenuContainer;
and example of another one:
import { useDetectClick } from 'src/utils/useDetectClick';
import TaskMenu from 'src/components/presentational/menus/TaskMenu';
const TaskMenuContainer = () => {
const [isActive, setIsActive] = useDetectClick(false);
const onClick = (): void => {
setIsActive(!isActive);
};
return (
<>
<TaskMenu onClick={onClick} isActive={isActive} />
</>
);
};
export default TaskMenuContainer;
The only difference is Component name which they are returning. It look's like i'm repeating myself. I am wondering how to make it Reusable (if only it's possible). I thought about passing component (taskMenu etc.) to one universal Menu component as a prop, but i can't find any similar issue anywhere in web and don't know how to do it.
EDIT:
import { useDetectClick } from 'src/utils/useDetectClick';
interface Props{
Component:React.ReactNode;
}
const ClickableContainer : React.FunctionComponent<Props> = ({Component}) => {
const [isActive, setIsActive] = useDetectClick(false);
const onClick = (): void => {
setIsActive(!isActive);
};
return <Component onClick={onClick} isActive={isActive} />;
};
export default ClickableContainer;
When i am trying this solution, this error occurs:
JSX element type 'Component' does not have any construct or call signatures.
And it's underlining "Component".
EDIT - WORKING SOLUTION:
import { useDetectClick } from 'src/utils/useDetectClick';
interface NestedComponentProps {
onClick: () => void;
isActive: boolean;
}
interface Props {
Component: React.FC<NestedComponentProps>;
}
const Menu: React.FC<Props> = ({ Component }) => {
const [isActive, setIsActive] = useDetectClick(false);
const onClick = (): void => {
setIsActive(!isActive);
};
return <Component onClick={onClick} isActive={isActive} />;
};
export default Menu;

You can pass the component to use as a prop. The important (and somewhat silly) detail to remember here is that the variable name needs to be upper-case!
import { useDetectClick } from 'src/utils/useDetectClick';
const ClickableContainer = ({Component}) => {
const [isActive, setIsActive] = useDetectClick(false);
const onClick = (): void => {
setIsActive(!isActive);
};
return <Component onClick={onClick} isActive={isActive} />;
};
export default ClickableContainer;
You can then reuse that:
import TaskMenu from 'src/components/presentational/menus/TaskMenu';
import ColumnMenu from 'src/components/presentational/menus/ColumnMenu';
import ClickableContainer from './ClickableContainer';
const TaskMenuContainer = () => <ClickableContainer Component={TaskMenu} />;
const ColumnMenuContainer = () => <ClickableContainer Component={ColumnMenu} />;

Related

Passing useState data from one functional component to another

I have two function components with useState in two different files in my project. I want to display the url on my FaceRecognition component if I set fetchSuccess to true.
const ImageLinkForm = () => {
const [url, setUrl] = useState("");
const [fetchSuccess, setFetchSuccess] = useState(false);
const onInputChange = (event) => {
// I get the url and fetchSuccess is true
};
return (
<div>
// I return a form that allowed me to make the fetch call
</div>
);
};
export default ImageLinkForm;
const FaceRecognition = () => {
return (
<div>
{/* if fetchSuccess */}
<img src=url />
</div>
);
};
export default FaceRecognition;
This really depends on how these components are hierarchically related but one easy-ish option is to use the context API
// context/image.js
import { createContext, useState } from "react";
export const ImageContext = createContext({ fetchSuccess: false });
export const ImageContextProvider = ({ children }) => {
const [fetchSuccess, setFetchSuccess] = useState(false);
const setSuccessful = () => {
setFetchSuccess(true);
};
return (
<ImageContext.Provider value={{ fetchSuccess, setSuccessful }}>
{children}
</ImageContext.Provider>
);
};
Your components can then use the context to read the value...
import { useContext } from "react";
import { ImageContext } from "path/to/context/image";
const FaceRecognition = () => {
const { fetchSuccess } = useContext(ImageContext);
return <div>{fetchSuccess && <img src="url" />}</div>;
};
and write the value...
import { useContext, useState } from "react";
import { ImageContext } from "path/to/context/image";
const ImageLinkForm = () => {
const [url, setUrl] = useState("");
const { setSuccessful } = useContext(ImageContext);
const onInputChange = (event) => {
// I get the url and fetchSuccess is true
setSuccessful();
};
return (
<div>{/* I return a form that allowed me to make the fetch call */}</div>
);
};
The only thing you need to do is wrap both these components somewhere in the hierarchy with the provider
import { ImageContextProvider } from "path/to/context/image";
const SomeParent = () => (
<ImageContextProvider>
<ImageLinkForm />
<FaceRecognition />
</ImageContextProvider>
);

Need to call an alert message component from action in react

I've created a common component and exported it, i need to call that component in action based on the result from API. If the api success that alert message component will call with a message as "updated successfully". error then show with an error message.
calling service method in action. is there any way we can do like this? is it possible to call a component in action
You have many options.
1. Redux
If you are a fan of Redux, or your project already use Redux, you might want to do it like this.
First declare the slice, provider and hook
const CommonAlertSlice = createSlice({
name: 'CommonAlert',
initialState : {
error: undefined
},
reducers: {
setError(state, action: PayloadAction<string>) {
state.error = action.payload;
},
clearError(state) {
state.error = undefined;
},
}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const error = useSelector(state => state['CommonAlert'].error);
const dispatch = useDispatch();
return <>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() =>
dispatch(CommonAlertSlice.actions.clearError())} />
{children}
</>
}
export const useCommonAlert = () => {
const dispatch = useDispatch();
return {
setError: (error: string) => dispatch(CommonAlertSlice.actions.setError(error)),
}
}
And then use it like this.
const App: React.FC = () => {
return <CommonAlertProvider>
<YourComponent />
</CommonAlertProvider>
}
const YourComponent: React.FC = () => {
const { setError } = useCommonAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <> ... </>
}
2. React Context
If you like the built-in React Context, you can make it more simpler like this.
const CommonAlertContext = createContext({
setError: (error: string) => {}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const [error, setError] = useState<string>();
return <CommonAlertContext.Provider value={{
setError
}}>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
{children}
</CommonAlertContext.Provider>
}
export const useCommonAlert = () => useContext(CommonAlertContext);
And then use it the exact same way as in the Redux example.
3. A Hook Providing a Render Method
This option is the simplest.
export const useAlert = () => {
const [error, setError] = useState<string>();
return {
setError,
renderAlert: () => {
return <MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
}
}
}
Use it.
const YourComponent: React.FC = () => {
const { setError, renderAlert } = useAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <>
{renderAlert()}
...
</>
}
I saw the similar solution in Antd library, it was implemented like that
codesandbox link
App.js
import "./styles.css";
import alert from "./alert";
export default function App() {
const handleClick = () => {
alert();
};
return (
<div className="App">
<button onClick={handleClick}>Show alert</button>
</div>
);
}
alert function
import ReactDOM from "react-dom";
import { rootElement } from ".";
import Modal from "./Modal";
export default function alert() {
const modalEl = document.createElement("div");
rootElement.appendChild(modalEl);
function destroy() {
rootElement.removeChild(modalEl);
}
function render() {
ReactDOM.render(<Modal destroy={destroy} />, modalEl);
}
render();
}
Your modal component
import { useEffect } from "react";
export default function Modal({ destroy }) {
useEffect(() => {
return () => {
destroy();
};
}, [destroy]);
return (
<div>
Your alert <button onClick={destroy}>Close</button>
</div>
);
}
You can't call a Component in action, but you can use state for call a Component in render, using conditional rendering or state of Alert Component such as isShow.

Static generation with Next.js: Pass page props to Layout component

I am trying to use getStaticProps for my Layout component as described here, but do struggle to solve this for my specific case:
_app.tsx
const NoCheck: React.FC = ({ children }) => <>{children}</>
function App({ Component, pageProps }: AppProps) {
const neoPage = Component as NeoPage
const LayoutComponent = neoPage.Layout || Layout
const { withAuthCheck = true } = neoPage
const CheckAuthComponent = withAuthCheck ? CheckAuth : NoCheck
return (
<CommonProviders pageProps={pageProps} overmindConfig={config}>
<CheckAuthComponent>
<LayoutComponent>
<Component {...pageProps} />
</LayoutComponent>
</CheckAuthComponent>
</CommonProviders>
)
}
export default App
LayoutUnauthorized.tsx
export const LayoutUnauthorized: React.FC<Props> = ({
children,
systemNormal,
}) => {
return (
<Flex>
<SystemStatus systemNormal={systemNormal} />
{children}
</Flex>
)
}
Login.tsx (in /page)
export const Login: NeoPage = () => {
return (
...
)
}
Login.Layout = LayoutUnauthorized
Login.withAuthCheck = false
export async function getStaticProps() {
const res = await fetch("https://company.com")
const systemNormal = await res.ok
return {
props: {
systemNormal,
},
revalidate: 1,
}
}
So Layout has a property I would like getStaticProps to pass. How can this be achieved?

Proper way to setup useContext and useState with React.js?

I have contexts/RoomContext.tsx:
import { useState, createContext } from 'react';
const RoomContext = createContext([{}, () => {}]);
const RoomProvider = (props) => {
const [roomState, setRoomState] = useState({ meetingSession: null, meetingResponse: {}, attendeeResponse: {} })
return <RoomContext.Provider value={[roomState, setRoomState]}>
{props.children}
</RoomContext.Provider>
}
export { RoomContext, RoomProvider }
Then in my component, RoomPage.tsx, I have:
const RoomPageComponent = (props) => {
const router = useRouter()
const [roomState, setRoomState] = useContext(RoomContext);
useEffect(() => {
const createRoom = async () => {
const roomRes = await axios.post('http://localhost:3001/live')
console.log('roomRes', roomRes)
setRoomState(state => ({ ...state, ...roomRes.data }))
}
if (router.query?.id) {
createRoom()
}
}, [router])
return <RoomPageWeb {...props} />
}
export default function RoomPage(props) {
return (
<RoomProvider>
<RoomPageComponent {...props} />
</RoomProvider>
)
}
But I get a complaint about the setRoomState:
This expression is not callable.
Type '{}' has no call signatures.
The issue here is that you are trying to use RoomContext in a component(RoomPage) which doesn't have RoomContext.Provider, higher up in the hierarchy since it is rendered within the component.
The solution here to wrap RoomPage with RoomProvider
import { RoomProvider, RoomContext } from '../../contexts/RoomContext'
function RoomPage(props) {
const [roomState, setRoomState] = useContext(RoomContext);
useEffect(() => {
const createRoom = async () => {
const roomRes = await axios.post('http://localhost:3001/live')
console.log('roomRes', roomRes)
setRoomState(state => ({...state, ...roomRes.data}))
}
...
return (
<RoomPageWeb {...props} />
)
export default (props) => (
<RoomProvider><RoomPage {...props} /></RoomProvider>
)

Test react hooks state using Jest and React Hooks Library

I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});

Resources