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>
)
Related
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>
);
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.
What would be the correct way to test that a component has updated its parent context?
Say from the example below, after MsgSender has been clicked, how can I verify that MsgReader has been updated?
import React from 'react'
import { render, act, fireEvent } from '#testing-library/react'
const MsgReader = React.createContext()
const MsgWriter = React.createContext()
const MsgProvider = ({ init, children }) => {
const [state, setState] = React.useState(init)
return (
<MsgReader.Provider value={state}>
<MsgWriter.Provider value={setState}>{children}</MsgWriter.Provider>
</MsgReader.Provider>
)
}
const MsgSender = ({ value }) => {
const writer = React.useContext(MsgWriter)
return (
<button type="button" onClick={() => writer(value)}>
Increment
</button>
)
}
describe('Test <MsgSender> component', () => {
it('click updates context', async () => {
const { getByRole } = render(
<MsgProvider init={1}>
<MsgSender value={2} />
</MsgProvider>,
)
const button = getByRole('button')
await act(async () => fireEvent.click(button))
// -> expect(???).toBe(2)
})
})
The cleanest way I've managed to come up with is to manually set the *.Providers, but I'm wondering if this is perhaps the wrong way to go about it.
it('click updates context with overrides', async () => {
let state = 1
const setState = (value) => {
state = value
}
const { getByRole } = render(
<MsgReader.Provider value={state}>
<MsgWriter.Provider value={setState}>
<MsgSender value={2} />
</MsgWriter.Provider>
</MsgReader.Provider>,
)
const button = getByRole('button')
expect(state).toBe(1)
await act(async () => fireEvent.click(button))
expect(state).toBe(2)
})
You need to create a customRender which gives you the ability to assert the state like this:
function customRender(ui, { init, ...options }) {
const [state, setState] = React.useState(init);
function wrapper({ children }) {
return (
<MsgReader.Provider value={state}>
<MsgWriter.Provider value={setState}>{children}</MsgWriter.Provider>
</MsgReader.Provider>
);
}
return {
...render(ui, { wrapper, ...options }),
state,
};
}
describe("Test <MsgSender> component", () => {
it("click updates context", async () => {
const { getByRole, state } = customRender(<MsgSender value={2} />);
const button = getByRole("button");
await act(async () => fireEvent.click(button));
expect(state).toBe(2)
});
});
i have defined context like below for setting states and is like below,
interface DialogsCtxState {
isDialogOpen: boolean;
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
itemVisible: boolean;
setItemVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialState: DialogsCtxState = {
isDialogOpen: false,
setIsDialogOpen: () => {},
itemVisible: false,
setItemVisible: () => {},
};
export const DialogsContext = React.createContext<
DialogsCtxState
>(initialState);
export const DialogsContextProvider: React.FC = ({ children }) => {
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(
false
);
const [itemsVisible, setItemsVisible] = React.useState<boolean>(
false
);
return (
<DialogsContext.Provider
value={{
isDialogOpen,
setIsDialogOpen,
itemVisible,
setItemVisible,
}}
>
{children}
</DialogsContext.Provider>
);
};
And i use this context in two components Uploadbutton and userbutton like below,
function UploadButton() {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext);
const onUpload = () => {
itemVisible && setItemVisible(false);
setIsDialogOpen(isDialogOpen => !isDialogOpen);
}
return (
<Button onClick={onUpload}/>
);
}
function UserButton() {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext);
const onAdd = () => {
isDialogOpen && setIsDialogOpen(false);
setItemVisible(prev => !prev);
}
return (
<Button onClick={onAdd}/>
);
}
the above snippet works fine. but i want to move the code withing onUpload and onAdd methods to DialogContext file which evaluates to something like below,
const onAdd = () => {
function1(); //where thiscontains the code in onAdd before snippet.
}
const onUpload = () => {
function2();//where this contains code in onUpload snippet before
}
what i have tried,
in file that contains DialogContext i tried something like below,
export const function1 = () => {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext); //error here
isDialogOpen && setIsDialogOpen(false);
setItemVisible(prev => !prev);
}
export const function2 = () => {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext); //error here
itemVisible && setItemVisible(false);
setIsDialogOpen(isDialogOpen => !isDialogOpen);
}
But i get the error react.usecontext is used in a function which is neither a react function component or custom react hook.
how can i fix this. could someone help me fix this. thanks.
It looks like the only use of the values returned from the context is to create the onUpload and onAdd functions. It will be a better approach to create the functions in the DialogsContextProvider component to pass them as value. Example
// context
interface DialogsCtxState {
onUpload: () => void;
onAdd: () => void;
};
const initialState: DialogsCtxState = {
onUpload: () => {},
onAdd: () => {}
};
export const DialogsContext = React.createContext<
DialogsCtxState
>(initialState);
The DialogsContextProvider component
// context provider
export const DialogsContextProvider: React.FC = ({ children }) => {
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(
false
);
const [itemsVisible, setItemsVisible] = React.useState<boolean>(
false
);
// onUpload function
const onUpload = useCallback(() => {
itemsVisible && setItemsVisible(false);
setIsDialogOpen((isDialogOpen) => !isDialogOpen);
}, [itemsVisible]);
// onAdd function
const onAdd = useCallback(() => {
isDialogOpen && setIsDialogOpen(false);
setItemsVisible((prev) => !prev);
}, [isDialogOpen]);
return (
<DialogsContext.Provider value={{ onAdd, onUpload}}>
{children}
</DialogsContext.Provider>
);
};
This is how it can be used in the UploadButton and UserButton components,
const UserButton: React.FC = () => {
const { onAdd } = React.useContext(DialogsContext)
// rest of the logic
}
const UploadButton: React.FC = () => {
const { onUpload } = React.useContext(DialogsContext)
// rest of the logic
}
Note There are multiple typos in your code so if we ignore that the error occurred when you took the custom function approach because useContext can only be used in functional components and custom hooks. To solve the issue you have to take a custom hook approach. For example
export const useOnUpload = () => {
const { isDialogOpen, setIsDialogOpen, itemsVisible, setItemsVisible,
} = React.useContext(DialogsContext);
const onUpload = useCallback(() => {
itemsVisible && setItemsVisible(false);
setIsDialogOpen((isDialogOpen) => !isDialogOpen);
}, [itemsVisible, setIsDialogOpen, setItemsVisible]);
return onUpload;
};
// usage
function UploadButton() {
const onUpload = useOnUpload();
// rest of the logic
}
// similarly you can create the onUseAdd hook
I'm trying to make a simple blog.
What I want to do is conditionally import a specific component based on the url params (id below).
However this code only renders Loading, it never changes. Why is this?
import Layout from "../../components/Layout";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
const LoadingPost = () => {
return <h1>Loading</h1>;
};
const Post = () => {
const router = useRouter();
const { id } = router.query;
const [loaded, setLoaded] = useState(false);
let PostToShow = LoadingPost;
useEffect(() => {
if (id) {
import(`../../posts/${id}.tsx`).then(_ => {
PostToShow = () => _;
setLoaded(true);
});
}
}, [id]);
const renderPost = () => {
if (loaded) {
return <PostToShow />;
}
};
return (
<Layout>
<h1>This would be a post</h1>
<h2>The id of this post would be: {id}</h2>
{renderPost()}
</Layout>
);
};
export default Post;
Here is how you can do this
import(`../../posts/${id}.tsx`).then(module=> {
PostToShow = module.default;
setLoaded(true);
});
You're doing the following:
import(`../../posts/${id}.tsx`).then(_ => {
PostToShow = () => _;
setLoaded(true);
});
So you're changing the value for the PostToShow variable, then setting the state - which triggers a new render. However on ever render you do:
let PostToShow = LoadingPost;
Hence you always render the LoadingPost component.
The issue here is that we're importing a module, not just a component.
import Layout from "../../components/Layout";
import { useEffect, useState } from "react";
const LoadingPost = () => {
return <h1>Loading</h1>;
};
const Post = ({ pageProps }) => {
const { id } = pageProps;
const [loaded, setLoaded] = useState(false);
const [Component, setComponent] = useState(LoadingPost);
useEffect(() => {
if (id) {
import(`../../posts/${id}.tsx`).then(_ => {
setComponent(_);
setLoaded(true);
});
}
}, [id]);
const renderPost = () => {
if (loaded) {
// #ts-ignore
const Comp = Component.default;
// #ts-ignore
return <Comp />;
}
};
return (
<Layout>
<h1>This would be a post</h1>
<h2>The id of this post would be: {id}</h2>
{renderPost()}
</Layout>
);
};
Post.getInitialProps = ctx => {
const { id } = ctx.query;
return {
pageProps: {
id,
},
};
};
export default Post;
As other people mentioned, we can do setComponent(_.default) either, but I couldn't get that working as it gives: Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object..
The above works as intended: