I am trying to test a workflow with a React app. When all fields are filled withing a workflow step, user is able to click to the "next" button. This action registers a state in a reducer and changes the URL to go to the next workflow step.
According to the RTL documentation, I wrap my component under test in a store provider and a connected router using this function:
export const renderWithRedux = (ui: JSX.Element, initialState: any = {}, route: string = "/") => {
// #ts-ignore
const root = reducer({}, { type: "##INIT" })
const state = mergeDeepRight(root, initialState)
const store = createStoreWithMiddleWare(reducer, state)
const history = createMemoryHistory({ initialEntries: [route]})
const Wrapper = ({ children }: any) => (
<Provider store={store}>
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</Provider>
)
return {
...render(ui, { wrapper: Wrapper }),
// adding `store` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
store
}
}
Unlike in the documentation, I amm using connected-react-router, not react-router-dom, but I've seen some people using connected-react-router with RTL on the web so I don't think the problem come from here.
The component under test is wrapped in a withRouter function, and I refresh the URL via the connected react router push function, dispatching via the redux connectfunction:
export default withRouter(
connect<any, any, any>(mapStateToProps, mapDispatchToProps, mergeProps)(View)
)
Everything work well in production, but the page doesn't refresh when I fire a click event on the "next" button. Here is the code of my test (to make it easier to read for you, I have filled all field and enable the "next" button):
const {
container,
queryByText,
getAllByPlaceholderText,
getByText,
debug,
getAllByText
} = renderWithRedux(<Wrapper />, getInitialState(), "/workflow/EXAC")
await waitForElement(
() => [getByText("supplierColumnHeader"), getByText("nextButton")],
{ container }
)
fireEvent.click(getByText("nextButton"))
await waitForElement(
() => [getByText("INTERNAL PARENT"), getByText("EXTERNAL PARENT")],
{ container }
)
Any clue of what is going wrong here?
Related
In my react app, I have some complex component tree.
In this component tree, I have a <Footer/> component with buttons. I also have <SomeComponent/> component elsewhere in the tree. This component is actually loaded from some dynamic code and is not always the same (similar to some widget engine, where the container is handled by the app engine, and the content is dynamically loaded). It means the context has no knowledge of what are actually the components.
In order to plug everything else, I have a custom react context that holds some fields and methods, which is exposed trough a custom useMyContext hook.
This is working quite well except one remaining issue :
In my <Footer /> I have a button that should call something inside the <SomeComponent/> component. As an example I may have a 'Refresh' button that should ask the component to get latest data.
Basically I have this react tree:
App
SomeContextProvider
Footer
RefreshButton
Deep/Nested/Component/Structure
SomeComponent
(contains a refresh function)
How can I call the refresh function in my component from the footer ?
I tried to play with forwarding refs and useImperativeHandler hook, which may work, but the deep nesting of component tree leads to a big mess of forwarding refs.
I also tried to extend the context provider, but I didn't found a way to "reverse" the callback (context can react to Refresh button action, but I cannot react to this in sibling branch of the component tree).
How could I handle this ?
PS: if it matters, I'm using react 16.13.1 and typescript 4.5
I think I have the start of a clean solution.
Basically, I can handle my scenario by implementing a subscribe/unsubscribe pattern hold by by app context.
This way I can emit some kind of event from my outer context, and let components in the tree subscribe and handle the events as needed.
Some repro : https://codesandbox.io/s/infallible-chaplygin-79c3gq?file=/src/App.tsx.
Relevant parts below:
Custom react context
type Subscribe = (cb: () => void) => () => void;
type AppContextData = {
subscribe: Subscribe;
onSubmit: () => void;
};
const AppContext = createContext<AppContextData | undefined>(undefined);
const useAppContext = (): AppContextData => {
const context = useContext(AppContext);
if (!context)
throw new Error(`useAppContext must be used within a AppContextProvider`);
return context;
};
Container component
const AppContextProvider: React.FC<PropsWithChildren<{}>> = ({ children }) => {
const subscribtions: (() => void)[] = [];
const subscribe: Subscribe = (cb) => {
subscribtions.push(cb);
return () => {
subscribtions.splice(subscribtions.indexOf(cb), 1);
};
};
const emitSubmit = () => {
subscribtions.forEach((cb) => cb());
};
const appContext: AppContextData = {
subscribe,
onSubmit: emitSubmit
};
return (
<AppContext.Provider value={appContext}>{children}</AppContext.Provider>
);
};
App
export default function App() {
return (
<AppContextProvider>
<div className="App">
<Main />
<Footer />
</div>
</AppContextProvider>
);
}
And finally, subscription and submission trigger:
Component with button
const Footer: React.VFC = () => {
const { onSubmit } = useAppContext();
return <button onClick={onSubmit}>Submit</button>;
};
Component that subscribes (and unsubscribe thanks to react effect)
const Main: React.VFC = () => {
const [myString, setMyString] = useState("initial");
const context = useAppContext();
useEffect(() => {
return context.subscribe(() => setMyString("from context"));
}, [context]);
return <p>{myString}</p>;
};
Why? One could have multiple components that needs to access a slice of the global app state (here provided by the hook useGlobalStore), for example:
// Component 1
export cost NavbarUserInfo = () => {
// Retrieve the current auth state
const auth = useGlobalStore(state => state.auth);
return (<p>{auth.username}</p>);
}
// Component 2
export cost UserDetails = () => {
// Retrieve the current auth state AGAIN!
const auth = useGlobalStore(state => state.auth);
return ({ /* show user info */ });
}
// Component 3 using the same state slice
Passing down props is not an option. The three (at least) problems of this approach:
Repeat access to useGlobalStore to retrieve the same state slice over and over
Coupling two components to a given state pattern library
Need to change many components for refactoring the state slice design
Is possible (or it's considered a bad practice) to mix the state pattern with context API? That is, inside my root component, expose a context with all my global state?
import { initialState, useGlobalStore} from './store.ts';
export cost App = () => {
const GlobalContext = React.createContext(initialState);
const state = useGlobalStore(state => state);
return (
<GlobalContext.Provider value={state}>
{/* now global state is available with the context API */}
</GlobalContext.Provider>
);
}
We have a simple setup involving a Context API provider wrapping the _app.tsx page. The flow we're trying to implement is:
On a page we collect some data from an API by using getServerSideProps
We send the data to the page through the props
We update the context from the page (the context provider is wrapping the _app.tsx as mentioned above)
The context is updated and all the children components can access the data in it
So we have a pretty standard setup that works correctly on the client side. The problem is that the updated context values are not being used to SSR the pages.
Context API
const ContextValue = createContext();
const ContextUpdate = createContext();
export const ContextProvider = ({children}) => {
const [context, setContext] = useState({});
return <ContextValue.Provider value={context}>
<ContextUpdate.Provider value={setContext}>
{children}
</ContextValue.Provider>
</ContextUpdate.Provider>
}
export const useContextValue = () => {
const context = useContext(ContextValue);
return context;
}
export const useContextUpdate = () => {
const update = useContext(ContextUpdate);
return update;
}
Then we have on _app.jsx:
...
return <ContextProvider>
<ContextApiConsumingComponent />
<Component {...pageProps} />
</Context>
And in any page, if we can update the context by using the hook provided above. For example:
export function Index({customData, isSSR}) {
const update = useContextUpdate();
if(isSSR) {
update({customData})
}
return (
<div>...</div>
);
}
export async function getServerSideProps(context) {
...
return {
props: { customData , isSSR}
};
};
And we can consume the context in our ContextApiConsumingComponent:
export function ContextApiConsumingComponent() {
const context = useContextValue()
return <pre>{JSON.stringify(context?.customData ?? {})}</pre>
}
The (very simplified) code above works fine on the client. But during the SSR, if we inspect the HTML sent to the browser by the server, we'll notice that the <pre></pre> tags are empty even though the context values are correctly sent to the browser in the __NEXT_DATA__ script section (they are precisely filled with the data on the browser after the app is loaded).
I've put together a GitHub repo with a minimum reproduction too.
Am I missing something?
As far as I understand, React doesn't wait for asynchronous actions to be performed during the SSR. Whereas setState is an asynchronous operation.
If you want it to be rendered during SSR, you need to provide an initialValue for useState hook.
export const ContextProvider = ({children, customData}) => {
const [context, setContext] = useState(customData);
// ...
}
I looked around and tried to find a solution with React router.
With V5 you can use <Promt />.
I tried also to find a vanilla JavaScript solution, but nothing worked for me.
I use React router v6 and histroy is replaced with const navigate = useNavigation() which doesn't have a .listen attribute.
Further v6 doesn't have a <Promt /> component.
Nevertheless, at the end I used useEffect clear function. But this works for all changes of component. Also when going forward.
According to the react.js docs, "React performs the cleanup when the component unmounts."
useEffect(() => {
// If user clicks the back button run function
return resetValues();;
})
Currently the Prompt component (and usePrompt and useBlocker) isn't supported in react-router-dom#6 but the maintainers appear to have every intention reintroducing it in the future.
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
useBackListener(({ location }) =>
console.log("Navigated Back", { location })
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.
I am a seasoned Angular developer who has now decided to learn React. To this end, I am rewriting one of my web apps in React. I have been reading about replacing Redux with the Context API and the useReducer() hook, but in my case I don't think this would be a good idea since my state is quite large and I don't yet have the React skill to mitigate any performance problems that might arise.
So I have decided to stick with Redux.
I now have the problem that I have a context that I need to make use of in my Redux store. My SocketProvider looks like this:
export const SocketProvider = ({
children,
endpoint
}) => {
const [socket, setSocket] = useState();
const [connected, setConnected] = useState(false);
useEffect(() => {
// set socket and init
}, [isAuthenticated]);
return (
<SocketContext.Provider
value={{
socket,
connected
}}
>
{children}
</SocketContext.Provider>
)
}
I would like to use the socket value from the SocketContext within my Redux store, like in this async action:
export const fetchComments = (issue: Issue): AppThunk => async dispatch => {
try {
dispatch(getCommentsStart())
const comments = socket.emit('getComment', issue.comments_url) // how can I get this socket?
dispatch(getCommentsSuccess({ issueId: issue.number, comments }))
} catch (err) {
dispatch(getCommentsFailure(err))
}
}
const App = () => {
return (
<SocketProvider endpoint={process.env.REACT_APP_API_SOCKET_ENDPOINT}>
<Provider store={store}>
{ /* my components */}
</Provider>
</SocketProvider>
);
}
I receive errors like this when I try to grab the socket context in my store code:
Unhandled Rejection (Error): Invalid hook call. Hooks can only be called inside of the body of a function component.
I can understand why this is happening, but I don't know the best way to solve it. I could keep all the thunks local to my components, or pass the context as a parameter in the async actions, but neither of these is really satisfactory.
Suggestions?