Upgrading to React Router v6 and removed connected-react-router so now I'm needing a different way to navigate to a URL once a saga completes. We used to dispatch using connected-react-router with history.push() but we now need a way to use the useNavigate() hook instead.
I can set up my sagas to return a Promise and use a .then() within my components in some places, but in others that won't work.
We have sagas checking for multiple statuses and the navigate only needs to trigger on one or two of the status values. It seems like there should be a better way to use the useNavigate() hook without having to pass it in with each action that's dispatched to update the status.
Is there some way to allow useNavigate() to be available globally? Here's an example where I can't use the useNavigate() hook and don't necessarily want to pass it in as a function either.
// Saga
function* watchStatus(
action: ActionType<typeof startActions.changeStatus>
): SagaIterator {
if (action.payload.status === "END_CREATE") {
const data: ReturnType<typeof getDataRequest> = yield select(
getDataRequest
);
} else if (action.payload.status === "END_VIEW") {
// HERE IS WHERE I WOULD NEED TO NAVIGATE TO A NEW ROUTE
}
}
// State transition logic
export const statusChanges: {
[S in Status]: {
readonly onNext: (state: RootState) => StartStatus;
};
} = {
FEE_ESTIMATE: {
onNext: () => "END_CREATE"
},
END_VIEW_REPORT: {
onNext: () => "END_VIEW"
}
};
Ideally I would probably want to separate out concerns, actions, and routes to avoid this problem, but a large chunk of the app I'm working on has already been written "toggling" on these statuses within the same route so I'm trying to figure out a good workaround.
I wound up using redux-first-history as a replacement for connected-react-router where it didn't make sense to switch to using the useNavigate hook.
Related
I've found lots of examples on v4 with the onLeave function. it seems this was discontinued after v4 however.
I've seen some <Prompt> examples but dont think thats what i'm looking for. if you have a case that will work i am open to it.
My Scenario:
React v16 app where i have multiple tables. I have a section in the store (global state) where i retain certain ui preferences. the tables paginate and reuse some of the same state info for pagination, sort, etc.
user story - user selects page 4, then navigates to another table, is still on page 4 (pagination is read from the store). I simple want to purge the values from state (im using redux so i will call an action to do this) but how can i trigger that, for example, on only a few routes in my app. this way i can reset it on leave and its ready by the time the user gets to the next table?
//edit for comment 1, basic example
const routes = [
{
path: "/one-thing",
component: OneThing,
},
{
path: "/two-thing",
component: TwoThing,
},
...
this is a huge app, but this might clarify. How can i run code, when i leave the path /one-thing?
we used to be able to do this in v4
{
path: "/two-thing",
component: TwoThing,
onLeave: doAThing,
},
Sounds like you could either listen for changes to the current location via the history object in a parent component.
history.listen
import { useHistory } from 'react-router-dom';
import { useEffect } from "react";
...
const history = useHistory();
useEffect(() => {
const unlisten = history.listen((location, action) => {
console.log('Route changed', { location, action });
// Apply route change logic, i.e. dispatch to store
});
return unlisten;
}, []);
Or use unmounting useEffect cleanup function in the component with table.
import { useEffect } from "react";
...
useEffect(() => {
return() => {
// component unmounting (maybe from route change)
// Apply route change logic, i.e. dispatch to store
};
}, []);
Inside a slice file we export all the the actions from that slice. For example:
export const {signoutUser, updateProfile, authenticateUser, clearUserState} = sliceName.actions;
And then we import useDispatch and particular actions from the slice or action file based on your folder structure. For example
import {clearUserState} from './slice';
import { useDispatch } from 'react-redux';
export const Component () {
const dispatch = useDispatch(clearUserState());
//rest component body
}
Now instead I am exporting a custom hook from the slice file like mentioned below:
export const useUserDispatch = () => {
const dispatch = useDispatch();
const userDispatch = {
signoutUser: (data) => dispatch(signoutUser(data)),
updateProfile: (data) => dispatch(updateProfile(data)),
authenticateUser: (data) => dispatch(authenticateUser(data)),
clearUserState: () => dispatch(clearUserState())
};
return {userDispatch}
};
And then i can just import that hook and use like
const {userDispatch}=useUserDispatch();
//inside component
<button onClick={userDispatch.clearUserState()}>Dispatch Button</button>
I just wanted to know if it's something that's not recommended in terms of redux way of writing code or am I doing anything wrong, it works perfectly fine though.
There is nothing wrong with your code. and the question can not be answered to pros and cons based on my experience, redux and all other open-source packages consider base common cases which people are using in the everyday app. There might be some suggestions for improvement but not best-case explanations for every app. you can just consider following and decide yourself
You can not use them as you mentioned useUserDispatch().clearUserState()
e.g.
<button onCleack={useDispatcher().clearUserState}>clear</button>
Hooks can be called conditionally so calling them as this level in the UI part might be conditionally canceled which is a really common case
every time this hook is called a hole new object is created for dispatchers
Also, useDispatch doesn't receive any argument and returns the nearest redux provider store.dispatch. see source code
Note: Redux suggests having one state for all of your apps and doesn't wrap part of your code with multiple providers.
Remember if you need this one of dispatcher (e.g. updateProfile) from some other part of the code, you may need to use this hook which is a waste of resources, or use it directly which shows a is a little bit of uncertainty and be not consistent from the other version (just a little is not a big case).
There are other options to handle these cases.
Remember what redux suggests for one provider, if you accept that you can also write your own dispatcher.
const storeDispatch = store.dispatch
// from slice file export dispatcher instead of action creator
const updateProfile = sliceName.actions.updateProfile;
export const updateProfileDispatcher = (data) => storeDispatch(updateProfile(data))
This can not only be used in your component but also can be used outside of react component inside the logic
Note: using a dispatcher outside the react is not the standard or recommended pattern and you might not want to use it.
You can also pass dispatch as the argument, something like thunk dispatcher
const updateProfile = dispatch => (data) => dispatch(updateProfile(data))
// or alongside of payload data
const updateProfile = (dispatch, data) => dispatch(updateProfile(data))
// also can set the default value of dispatch to store.dis
const updateProfile = (data, dispatch=storeDispatch) => dispatch(updateProfile(data))
// component
const dispatch = useDispatch()
<ProfileComponent onUpdate={updateProfile(dispatch)} />
I'm trying to learn why it's necessary to use useCallback in this situation:
function MyComponent() {
const navigation = useContext(NavigationContext);
const redirect = useCallback(() => {
navigation.navigate("Home");
});
useEffect(() => {
redirect();
}, [redirect]);
}
I can't use navigation directly inside useEffect() and also I can't refer the function without useCallback(). I don't know why I can use other objects like Firebase Context (from a Firestore instance) without any problem inside useEffect, but I can't use navigation directly.
From a quick look in the equivalent codebases:
Firebase Context provider is returned memoized
whilst the react-navigation one just returns the object.
I need to dispatch a Redux action every time there is a route change in my app. Unfortunately, these answers don't apply to my case, since I have to use BrowserRouter instead Router, and BrowserRouter does not take a history prop. Is there another way to do this? I am using V4.
Dennis' comment gave a working solution, using React hooks. Make a new component as follows:
const RouteChange = withRouter(({ location }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch({ type: "AN_ACTION" });
}, [dispatch, location.pathname]);
return null;
});
and just include it in the root of your application. The specified action will be dispatched whenever the route changes.
You can use componentwillunmount in each of your react class and it will work fine
This is not an issue but rather a question.
I wanted to use React solely for my Global state management and pass the todos through useReducer and useContext and I wonder if this is by any means a right way to go. I was called out by a react coder that this way the components rerender when they aren't supposed to but my element inspection shows only the changed component rerenders. Would please guide me as whether or not I can continue developing this way or have to revert back to Mobx or redux or many other third party state manager libraries.
Yes, you can and it's easier than ever thanks to the new hooks API! For very simple things like for instance, a global theme you can just create a context with React.createContext, and useContext.
For a more robust solution, you can actually implement a Flux architecture with a combination of useContext and useReducer. Here's one I made earlier.
// AcmeContext.js
import React, { useReducer, createContext } from 'react'
const AcmeContext = createContext({})
const actions = {
DO_SOMETHING: 'doSomething'
}
const actionCreators = dispatch => ({
updateComment: comment => {
dispatch({
type: actions.DO_SOMETHING,
payload: comment
})
}
})
// first paramter is your state, second is the action
let reducer = (currentState, { type, payload }) => {
switch (type) {
case actions.DO_SOMETHING:
// important: return a NEW new object for this context, don't change the old currentState
return { ...currentState, hello: payload }
default:
return
}
}
// this component wraps any of the child components that you want to share state with
function AcmeProvider({ children, initialState }) {
const [state, dispatch] = useReducer(reducer, initialState)
const actions = actionCreators(dispatch)
return (
<AcmeContext.Provider value={{ state, actions }}>
{children}
</AcmeContext.Provider>
);
}
export { AcmeContext, AcmeProvider }
Then, you wrap the component you want to provide the context to with the exported provider.
// App.jsx
import { AcmeProvider } from './AcmeContext'
import TestComponent from './TestComponent'
render((
<AcmeProvider initialState={{ hello: 'world' }}>
<TestComponent />
</AcmeProvider>
), document.querySelector('.app'))
Finally, you can call it from the child component.
// TestComponent.jsx
import { AcmeContext } from './AcmeContext'
export default () => {
const { state, actions } = useContext(AcmeContext)
return (
<div>
Hello {state.hello}!
<button onClick={() => actions.updateComment('me')}>Set response on onClick to 'me'</button>
</div>
)
}
This does have a couple of downsides to a full Redux implementation. You don't get the Redux dev tools and you don't get things like redux-thunk which means you'll have to add that logic to the component and get the component to update the context.
Yes you can totally use the default React APIs for full state management on a project. The introduction of hooks makes it easy to manage. useContext has slowly become my favourite hook because it removes the need for consumers and makes the JSX look a bit nicer.
If you are worried about things rerendering too many times, you can still use all of the tricks in the React Toolbox like React.memo.