I have react-redux app.
In my app I have many places where user can select item, like - select main menu in side bar, select item main menu, select tab.
Each one of this actions (select_manu, select_item) cause responses at several reducers.
I want to prevent reducers change the state when the selected-item is the same as the current.
My question is:
Where is the right place to ask the follwing:
if (currentTab!= selectedTab)
First option:
At the action-creator, by thunk. something like:
export const selectTab = (selectedTab) => {
return {
type: Const.SELECT_TAB,
selectedTab
}
};
export const tabClick = (tab) => {
return function (dispatch, getState) {
const currentTab = getState().active.tab;
if (currentTab!= tab) {
dispatch(selectTab(tab))
}
}
};
second option:
At the container, something like:
const mergeProps = (stateProps, dispatchProps, ownProps) => {
return {
onTabSelection: (tab) => {
if (tab != stateProps.tab)
dispatch(selectTab(tab));
}
Related
Redux Form has FieldArray field:
https://redux-form.com/6.0.0-rc.3/docs/api/fieldarray.md/
I am trying to delete multiple of items from it but remove() method only works for a single removal perhaps because each time the fields get one item smaller and the index determined by me is bigger than the fields array:
<MultiSelect
placeholder="Delete project group"
onChange={(v) => {
const diff = difference(addedGroups, v)
if (!isEmpty(diff)) {
const groupToDelete = diff[0]
forEach(projectsByGroup[groupToDelete], p => removeElement(addedProjects.indexOf(p)))
deleteGroup(groupToDelete)
}}
options={projectGroupNames}
value={addedGroups}
inline
/>
Where removeElement is fields.remove FieldArray function. How to remove correctly multiple items from FieldArray selectively?
Update:
I have also tried to use change in my reducers like that:
import { change } from 'redux-form'
export const deleteVariantSearchProjectGroup = (projectGroupGuid) => {
return (dispatch, getState) => {
const state = getState()
const projectsInGroup = state.projectsByProjectGroup[projectGroupGuid]
const allProjectFields = getProjectsFamiliesFieldInput(state)
const remainingProjectFields = allProjectFields.filter(projectField => !projectsInGroup.includes(projectField.projectGuid))
change(SEARCH_FORM_NAME, 'projectFamilies', remainingProjectFields)
dispatch({ type: UPDATE_VARIANT_SEARCH_ADDED_GROUPS, newValue: without(getState().variantSearchAddedProjectGroups, projectGroupGuid) })
}
}
I get correctly an array remainingProjectFields but then change(SEARCH_FORM_NAME, 'projectFamilies', remainingProjectFields) does not do anything.
I was not able to actually find a way to remove fields one by one with fields.remove but ultimately I solved it by using a reducer and updating Redux Form state using change method:
import { change } from 'redux-form'
export const deleteVariantSearchProjectGroup = (projectGroupGuid) => {
return (dispatch, getState) => {
const state = getState()
const projectsInGroup = state.projectsByProjectGroup[projectGroupGuid]
const allProjectFields = getProjectsFamiliesFieldInput(state)
const remainingProjectFields = allProjectFields.filter(projectField => !projectsInGroup.includes(projectField.projectGuid))
dispatch(change(SEARCH_FORM_NAME, 'projectFamilies', remainingProjectFields))
dispatch({ type: UPDATE_VARIANT_SEARCH_ADDED_GROUPS, newValue: without(getState().variantSearchAddedProjectGroups, projectGroupGuid) })
}
}
and deleteVariantSearchProjectGroup = deleteGroup in the very first jsx code snippet in the question.
I would like to detect when the user leaves the page Next JS. I count 3 ways of leaving a page:
by clicking on a link
by doing an action that triggers router.back, router.push, etc...
by closing the tab (i.e. when beforeunload event is fired
Being able to detect when a page is leaved is very helpful for example, alerting the user some changes have not been saved yet.
I would like something like:
router.beforeLeavingPage(() => {
// my callback
})
I use 'next/router' like NextJs Page for disconnect a socket
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function MyPage() {
const router = useRouter()
useEffect(() => {
const exitingFunction = () => {
console.log('exiting...');
};
router.events.on('routeChangeStart', exitingFunction );
return () => {
console.log('unmounting component...');
router.events.off('routeChangeStart', exitingFunction);
};
}, []);
return <>My Page</>
}
router.beforePopState is great for browser back button but not for <Link>s on the page.
Solution found here: https://github.com/vercel/next.js/issues/2694#issuecomment-732990201
... Here is a version with this approach, for anyone who gets to this page
looking for another solution. Note, I have adapted it a bit further
for my requirements.
// prompt the user if they try and leave with unsaved changes
useEffect(() => {
const warningText =
'You have unsaved changes - are you sure you wish to leave this page?';
const handleWindowClose = (e: BeforeUnloadEvent) => {
if (!unsavedChanges) return;
e.preventDefault();
return (e.returnValue = warningText);
};
const handleBrowseAway = () => {
if (!unsavedChanges) return;
if (window.confirm(warningText)) return;
router.events.emit('routeChangeError');
throw 'routeChange aborted.';
};
window.addEventListener('beforeunload', handleWindowClose);
router.events.on('routeChangeStart', handleBrowseAway);
return () => {
window.removeEventListener('beforeunload', handleWindowClose);
router.events.off('routeChangeStart', handleBrowseAway);
};
}, [unsavedChanges]);
So far, it seems to work pretty reliably.
Alternatively you can add an onClick to all the <Link>s yourself.
You can use router.beforePopState check here for examples
I saw two things when coding it :
Knowing when nextjs router would be activated
Knowing when specific browser event would happen
I did a hook that way. It triggers if next router is used, or if there is a classic browser event (closing tab, refreshing)
import SingletonRouter, { Router } from 'next/router';
export function usePreventUserFromErasingContent(shouldPreventLeaving) {
const stringToDisplay = 'Do you want to save before leaving the page ?';
useEffect(() => {
// Prevents tab quit / tab refresh
if (shouldPreventLeaving) {
// Adding window alert if the shop quits without saving
window.onbeforeunload = function () {
return stringToDisplay;
};
} else {
window.onbeforeunload = () => {};
}
if (shouldPreventLeaving) {
// Prevents next routing
SingletonRouter.router.change = (...args) => {
if (confirm(stringToDisplay)) {
return Router.prototype.change.apply(SingletonRouter.router, args);
} else {
return new Promise((resolve, reject) => resolve(false));
}
};
}
return () => {
delete SingletonRouter.router.change;
};
}, [shouldPreventLeaving]);
}
You just have to call your hook in the component you want to cover :
usePreventUserFromErasingContent(isThereModificationNotSaved);
This a boolean I created with useState and edit when needed. This way, it only triggers when needed.
You can use default web api's eventhandler in your react page or component.
if (process.browser) {
window.onbeforeunload = () => {
// your callback
}
}
Browsers heavily restrict permissions and features but this works:
window.confirm: for next.js router event
beforeunload: for broswer reload, closing tab or navigating away
import { useRouter } from 'next/router'
const MyComponent = () => {
const router = useRouter()
const unsavedChanges = true
const warningText =
'You have unsaved changes - are you sure you wish to leave this page?'
useEffect(() => {
const handleWindowClose = (e) => {
if (!unsavedChanges) return
e.preventDefault()
return (e.returnValue = warningText)
}
const handleBrowseAway = () => {
if (!unsavedChanges) return
if (window.confirm(warningText)) return
router.events.emit('routeChangeError')
throw 'routeChange aborted.'
}
window.addEventListener('beforeunload', handleWindowClose)
router.events.on('routeChangeStart', handleBrowseAway)
return () => {
window.removeEventListener('beforeunload', handleWindowClose)
router.events.off('routeChangeStart', handleBrowseAway)
}
}, [unsavedChanges])
}
export default MyComponent
Credit to this article
this worked for me in next-router / react-FC
add router event handler
add onBeforeUnload event handler
unload them when component unmounted
https://github.com/vercel/next.js/issues/2476#issuecomment-563190607
You can use the react-use npm package
import { useEffect } from "react";
import Router from "next/router";
import { useBeforeUnload } from "react-use";
export const useLeavePageConfirm = (
isConfirm = true,
message = "Are you sure want to leave this page?"
) => {
useBeforeUnload(isConfirm, message);
useEffect(() => {
const handler = () => {
if (isConfirm && !window.confirm(message)) {
throw "Route Canceled";
}
};
Router.events.on("routeChangeStart", handler);
return () => {
Router.events.off("routeChangeStart", handler);
};
}, [isConfirm, message]);
};
I have an issue where I have a simple React.Context that's populated after all the components mount. The problem is that because it happens after mount, nextjs does not see this data on initial render, and so there's noticeable flicker.
Here's the simple component that sets the Context:
export const SetTableOfContents = (props: { item: TableOfContentsItem }) => {
const toc = useContext(TableOfContentsContext);
useEffect(() => {
// Updates the React.Context after the component mount
// (since useEffects run after mount)
toc.setItem(props.item);
}, [props.item, toc]);
return null;
};
Here's the React.Context. It uses React state to store the TOC items.
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const [items, setItems] = useState<TableOfContents["items"]>([]);
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
setItems((items) => items.concat(item));
},
};
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Currently, it is not possible to set the React.Context before mount because React gives a warning---Cannot update state while render.
The only workaround I can think of is to use something other than React.state for the React.Context state---that way the component can update it any time it wants. But then the problem with that approach is that Context Consumers would no longer know that the items changed (because updates live outside the React lifecycle)!
So how to get the initial React.Context into the initial SSR render?
const items = [];
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
items[item.index] = item;
},
};
// this dep never changes.
// when you call this function, values never change
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Here's what I ended up doing:
render the app in getStaticProps using renderToString
use useRef for state in the Context instead of useState
the reason for doing this is because renderToString renders only the initial state. So if you update the Context using useState, it won't capture subsequent renders
update the Context on component initialization for the reason mentioned above
pass the Context an "escape hatch"---a function we can call to get the state calculated on the initial render
Yes, the whole thing seems like a giant hack! :-) I'm not sure if React.Context plays well with SSR :(
export const TableOfContentsProvider = (props: {
initialItems?: TableOfContentsItem[];
setItemsForSSR?: (items: TableOfContentsItem[]) => void;
children?: React.ReactNode;
}) => {
// use useRef for the reasons mentioned above
const items = useRef(props.initialItems || []);
// Client still needs to see updates, so that's what this is for
const [count, setCount] = useState(0);
const { setItemsForSSR } = props;
const setterValue = useMemo(
() => ({
setItem(item: TableOfContentsItem) {
if (!items.current.find((x) => x.id === item.id)) {
items.current.push(item);
items.current.sort((a, b) => a.index - b.index);
setCount((count) => count + 1);
setItemsForSSR?.(items.current);
}
},
}),
[setItemsForSSR]
);
const stateValue = useMemo(() => ({ items: items.current, count }), [count]);
return (
<TableOfContentsSetterContext.Provider value={setterValue}>
<TableOfContentsStateContext.Provider value={stateValue}>
{props.children}
</TableOfContentsStateContext.Provider>
</TableOfContentsSetterContext.Provider>
);
};
interface TableOfContentsSetterWorkerProps {
item: TableOfContentsItem;
setItem: (item: TableOfContentsItem) => void;
}
export class TableOfContentsSetterWorker extends React.Component<
TableOfContentsSetterWorkerProps,
{}
> {
constructor(props: TableOfContentsSetterWorkerProps) {
super(props);
// Need to do this on init otherwise renderToString won't record it
props.setItem(props.item);
}
render() {
return null;
}
}
/**
* Usage: use this as a child component when the parent needs to set the TOC.
*
* Exists so that a component can set the TOC without triggering
* an unnecessary render on itself.
*/
export function TableOfContentsSetter(props: { item: TableOfContentsItem }) {
const { setItem } = useContext(TableOfContentsSetterContext);
return <TableOfContentsSetterWorker item={props.item} setItem={setItem} />;
export const getStaticProps = async () => {
let initialTableOfContents: TableOfContentsItem[] = [];
const getItems = (items: TableOfContentsItem[]) => {
initialTableOfContents = [...items];
};
const app = () => (
<TableOfContentsProvider setItemsForSSR={getItems}>
<AppArticles />
</TableOfContentsProvider>
);
renderToString(app());
return {
props: {
initialTableOfContents,
},
};
};
I have a redux store with multiple teams.
const store = {
selectedTeamId: 'team1';
teams: {
team1: { ... },
team2: { ... },
team3: { ... },
},
};
At any given time a teamId is set.
Now given that I must select the team using the ID each time I call mapStateToProps(), I feel this is cumbersome.
Instead of doing this all the time:
mapStateToProps({ selectedTeamId, teams }) {
return {
team: teams[selectedTeamId],
}
}
Can I pre-process the store using some middleware instead of repeating this pattern in map state to props?
Approach suggested by Redux docs is to create a selector for currently active team and reuse it across all components
// selector itself is a pure function of state
// usually put in separate file, or in file with reducer
const activeTeamSelector = state => state.teams.teams[state.teams.selectedTeamId]
// in connect
const mapStateToProps = (state) => ({
activeTeam: activeTeamSelector(state),
})
That, of course, if you are using combineReducers and teams reducer is called teams in state. If you aren't, and selectedTeamId and teams are contained right in your store, following will work
const activeTeamSelector = state => state.teams[state.selectedTeamId]
Notice how I only had to change selector for this, and not every mapStateToProps in all the components
read more about Normalizing Store State and Computing Derived Data in Redux docs
Using a middleware for this scenario isn't performant (if I understood your question correctly :) ). I will outline 3 options you can use to achieve this:
Option 1
return both selectedTeamId and teams in mapStateToProps, this will allow you to find the team you need for each selected id:
mapStateToProps({ selectedTeamId, teams }) {
return {
selectedTeamId,
teams
}
}
That way you can access these props in render:
render() {
const { teams, selectedTeamId } = this.props;
return <Team team={teams.find(team => team.id === selectedTeamId)} />
}
Note: <Team /> is just a component I made for demonstration
Option 2
you can use reselect library to avoid recomputing this prop:
import { createSelector } from 'reselect'
const teams = state => state.teams;
const selectedTeamId = state => state.selectedTeamId;
const subtotalSelector = createSelector(
teams,
selectedTeamId,
(teams, selectedTeamId) => items.find(team => team.id === selectedTeamId)
)
Option 3
Create an action that will dispatch 'SELECT_TEAM' with the teamId
export function setSelectedTeam(id) {
return { type: types.SELECT_TEAM, payload: id };
}
Create a reducer for that type and return selectedTeam:
[types.SELECT_TEAM]: (state, payload)=> {
return {
...state,
selectedTeam: state.teams.find(team => team.id === payload.id)
};
},
That way you can have a selector for selectedTeam
export const getSelectedTeam = state => state.selectedTeam;
Hope it helps
I eventually used reselect, with thanks to the recommendation of #jank.
One of things I wanted to do was abstract away the need for selectors to appear in mapStateToProps. In order to do that, I wrapped redux connect. This allows insertion of a denormalizer function before mapStateToProps.
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
const getActiveTeamId = state => state.activeTeamId;
const getAllTeams = state => state.teams;
const teamSelector = createSelector(
getActiveTeamId,
getAllTeams,
(activeTeamId, teams) => teams[activeTeamId],
);
function denormalizer(mapStateToProps) {
return state => {
return mapStateToProps({ team: teamSelector(state) });
};
}
export default function reConnect(mapStateToProps = null, actions = null) {
const denormalizedMapStateToProps = denormalizer(mapStateToProps);
return function callConnect(Component) {
return connect(denormalizedMapStateToProps, actions)(Component);
};
}
I did the basic redux todolist tutorial and it worked but I wanted to get to know the code by making a small change.
I changed:
actions/index.js
let nextTodoId = 0
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
}
}
to this:
let nextTodoId = 0
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
payload: {
id: nextTodoId++,
text: text
}
}
}
And I got the adding a todo working with that but a strange side effect has occurred in the toggleTodo - There are no console errors but clicking a todo list item is supposed to toggle it between being completed (visually has a strike through the text) and being not completed. Clicking a list item now has no effect.
I'm struggling to pass this reducer an action which has a defined id.
reducers/todos.js:
This is the code which calls the toggleTodo(id) reducer (look for the arrow pointing and saying "HERE"):
containers/visibleTodoList.js:
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id)) <-------------HERE
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
So the id is coming from onTodoClick.
components/TodoList.js:
So I pause it there and see the value of onTodoClick():
Where in the original code was it setting onTodoClick.id so I can repeat it again to get the onTodoClick.id to be defined (thus hopefully causing the clicking a todo item to toggle successfully).
You are looking for state.id !== action.id but you pass it as action.payload.id do the following:
case 'TOGGLE_TODO':
if (state.id !== action.payload.id) {
return state
}