React: Accessing context from within a Redux store - reactjs

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?

Related

Next.js + SSR not updating the Context API

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);
// ...
}

Using the Context API as a way of mimicking useSelector and useDispatch with redux v5

I'm working on a React project where I'm constrained to using React Redux v5, which doesn't include useDispatch and useSelector.
Nonetheless I really would like to have these hooks (or something like them) available in my app.
Therefore, I've created a wrapper component at the top level of the app which I connect using redux's connect function.
My mapStateToProps and mapDispatchToProps then just look like this:
const mapDispatchToProps = (dispatch: DispatchType) => {
return {
dispatch,
};
};
const mapStateToProps = (state: StateType) => {
return {
state,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);
In my wrapper component, I then pass the dispatch and the state into the value:
<DispatchContext.Provider value={{ state, dispatch }}>
{children}
</DispatchContext.Provider>
Finally, I have a hook that looks like this:
const useSelectAndDispatch = () => {
const context = useContext(DispatchContext);
if (context === null) {
throw new Error("Please use useDispatch within DispatchContext");
}
const { state, dispatch } = context;
function select(selector) {
return selector(state);
}
return { dispatch, select };
};
I then use dispatch and selector in my components via useSelectAndDispatch.
I was wondering if this is an appropriate way to go about this issue, and whether I can expect any performance problems. I am using reselect, and have a good understanding of memoization. I'm just looking for opinions, since I've heard that the redux team held off implementing useDispatch and useSelector for a long time because of performance issues.
Many thanks for any opinions!
This will cause significant peformance problems. Your mapStateToProps is using the entire state object, so every time anything changes in the state, the provider must rerender. And since the provider rerendered with a new value, so too must every component that consumes the context. In short, you will be forcing most of your app to rerender anytime anything changes.
Instead of using mapStateToProps and mapDispatchToProps, i would go back to the actual store object, and build your hooks from that. Somewhere in your app is presumably a line of code that says const store = createStore(/* some options */).
Using that store variable, you can then make some hooks. If i can assume that there's only one store in your app, then the dispatch hook is trivial:
import { store } from 'wherever the store is created'
export const useMyDispatch = () => {
return store.dispatch;
}
And the selector one would be something like this. It uses .subscribe to be notified when something in the store changes, and then it uses the selector to pluck out the part of the state that you care about. If nothing changed, then the old state and new state will pass an === check, and react skips rendering. If it has changed though, the component renders (only the component that called useMySelect plus its children, not the entire app)
export const useMySelector = (selector) => {
const [value, setValue] = useState(() => {
return selector(store.getState());
});
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newValue = selector(store.getState());
setValue(newValue);
});
return unsubscribe;
}, []);
return value;
}

React - Error handling useState(). How to reset errors?

Introduction
I am handling all the errors in my app. I have noticed there are different types of errors that can be handled in React...
Errors with rendering (undefined props, ...), which can be handled with an Error Boundary Component, with custom Fallback Components.
Errors coming from the backend, which I want to display in a toast with i18n.js for translating the messages.
Is it common to combine these two types of error handling in React apps?
Displaying errors in a toast
I have decided to create my own React Context which renders toasts for notifications, errors, etc. Here is my implementation:
/* eslint-disable react/prop-types */
import React, {
createContext,
useReducer,
useMemo,
} from "react";
import toastReducer, {
initialState,
actionCreators,
} from "./helpers/reducers/toastReducer";
import { TOAST_TYPES, DEFAULT_TOAST_DURATION } from "../../utils/toast";
import Toast from "../../components/Toast";
const ToastContext = createContext(null);
export default ToastContext;
export function ToastProvider({ children }) {
const [toast, dispatch] = useReducer(toastReducer, initialState);
const open = (
type = TOAST_TYPES[0],
message,
duration = DEFAULT_TOAST_DURATION,
action = undefined
) => {
dispatch(actionCreators.open(type, message, duration, action));
};
const close = () => {
dispatch(actionCreators.close());
};
const value = useMemo(() => ({
open,
close,
}), []);
return (
<ToastContext.Provider value={value}>
{children}
<Toast
visible={toast.visible}
type={toast.type}
duration={toast.duration}
action={toast.action}
onDismiss={close}
>
{toast.message}
</Toast>
</ToastContext.Provider>
);
}
I have also implemented my useToast() hook that consumes this context.
Fetching data from server
As I am using hooks, I am saving the errors in a state "error" on each custom hook that fetches data from my DB.
// Based on the library "useQuery"
const useFetchSomeData = ({ fetchOnMount = true } = {}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cursor = useRef(new Date());
const isFetching = useRef(false);
const fetchData = async () => {
if (isFetching.current) return;
isFetching.current = true;
setLoading(true);
try {
const data = await api.fetchData(cursor.current);
} catch(err) {
setError(err);
} finally {
setLoading(false);
isFetching.current = false;
}
}
useEffect(() => {
if (fetchOnMount) {
fetchData();
}
}, []);
return {
data,
loading,
error,
fetchData
}
}
And in my components I just do:
const { data, error, loading } = useFetchData();
const toast = useToast();
const { t } = useI18N();
useEffect(() => {
if (error) {
toast.open("error", t(err.code));
}
}, [error, t]);
Questions
As you can see, in my components, I am just using a useEffect for sending the translated errors to my toast context. But... what if I get the same error response from the server twice? I mean, the error is not reset to null in the custom hook... how can I do that?
Is this implementation robust (in your opinion) and typical in these situations?
Honestly I'm a bit confused, I think I understand what you are trying to do but it doesn't have to be this complicated.
You should wrap your entire app with a ToastContainer or ToastContext (whatever you want to call it) and import your toast whenever needed then call Toat.show("Error: ..."). Checkout the library react-toastify for more details, maybe this library could help you. You wrap your project at the app entry level and never have to worry about it again.
As far as error handling, when creating a function to fetch data you should expect one error at a time and handle it appropriately. I personally think it's best to return the error to the original component that called the fetch function and handle it there. It's easier to keep track of things this way in my opinion, but what you're doing is not wrong.
Error handling takes time and thinking ahead, I'm not gonna deny it, but it makes a big different between a badly constructed app and a well thought out app.
I'm also not completely sure why you are using const isFetching = useRef(false); when you could just use const [isFetching, setIsFecthing] = useState(false), this is what useState is for. Same for useMemo. I think things could be leaner here, as you app grows and becomes more complex it'd easier to maintain.

why react hook init before redux props

this is some kind of code so be gentle :D
i have some page that use react hooks and redux and the redux state work
i checked it it load data. the scenario is i have page that load input boxes from redux thunk and save the changes with hooks and redux thunk
const MyComponent = (props) => {
const { reduxProp } = props
const [t1, setT1] = useState('')
useEffect( () => {
reduxActionLoadData // t1 , t2 data
setT1(reduxProp.t1)
setT2(reduxProp.t2)
},[])
return (
<div>
<input value={t1} onChange={ (e) => { setT1(e.target.value) } />
<input value={t2} onChange={ (e) => { setT2(e.target.value) } />
</div>
)
}
const mapStateToProp = (state) => (
{
reduxProp: state.reduxProp
})
const mapDispatchToProp = (dispatch) => (
{
reduxActionLoadData = connectActionCreators(reduxActionLoadDataAction, dispatch)
})
export default const connect(mapStateToProps, mapDispatchToProps)(MyComponent)
when i check props it is loaded, redux state is loaded too
but hooks init with empty value
i try to init them with reduxProp.t1, reduxProp.t2 no luck there too
the thing is when i run app it works fine but when i refresh the page it fails
and sometimg it fails when i run app too
i try to use loading state and do stuff after loading = true no luck the either
For one, your mapDispatchToProp currently does not seem to do anything, since with the = it will be parsed as a method body, not as an object - so you are not connecting your actions at all. It should look more like this:
const mapDispatchToProp = (dispatch) => (
{
reduxActionLoadData: connectActionCreators(reduxActionLoadDataAction, dispatch)
})
Then you should instead be using the map object notation:
const mapDispatchToProp = {
reduxActionLoadData: reduxActionLoadDataAction
}
and the you also have to actually use that in your component:
useEffect( () => {
props.reduxActionLoadData()
},[])
That will still create an asynchronous effect, so reduxProp.t1 will not change immediately.
You will probably have to update those later in a separate useEffect.
useEffect( () => {
setT1(reduxProp.t1)
},[reduxProp.t1])
All that said, there is no reason to use connect at all, since you could also use the useSelector and useDispatch hooks. connect is a legacy api that you really only need to use with legacy class components.
Generally I would assume that you are right now learning a very outdated style of Redux by following an outdated tutorial. Modern Redux does not use switch..case reducers, ACTION_TYPE constants, hand-written action creators, immutable reducer logic or connect - and it is maybe 1/4 of the code of old-style Redux.
Please follow the official Redux tutorial for an understanding of mondern Redux. https://redux.js.org/tutorials/essentials/part-1-overview-concepts

React-testing-library with connected react router together

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?

Resources