I'm trying to access a Container's props (that were passed in from Redux state) from within mapDispatchToProps (mDTP). My container is responsible for dispatching an action to fetch data, but the action that it dispatches needs access to a piece of redux state. That state will be used as a field in the HTTP header.
The redux state that I am trying to access is languages. I want to then pass it in as a header to mDTP
Here is my container:
const ExperienceListContainer = ({ list, loading, failed, readList, languages }) => {
useEffect(() => {
readList()
}, [])
const apiRequestParams = {
endpoint: 'GetToursByCity',
count: 10,
step: 10,
cityid: 'e2e12bfe-15ae-4382-87f4-88f2b8afd27a',
startdate: '2018-12-20 8:30:00',
enddate: '2018-12-20 22:15:00',
sort_by: 'rating',
guests: 10,
min_price: 1,
max_price: 100.50,
time_of_day: 1,
sort_by: 'rating',
categories: '',
languages: languages
}
return <ExperienceList {...{ list, loading, failed, languages }} />
}
ExperienceListContainer.propTypes = {
list: T.arrayOf(T.object).isRequired,
limit: T.number,
loading: T.bool,
failed: T.bool,
readList: T.func.isRequired,
filter: T.shape({
interests: T.arrayOf(T.shape({
id: T.number.isRequired,
text: T.string.isRequired
})),
languages: T.arrayOf(T.shape({
id: T.number.isRequired,
text: T.string.isRequired
})),
}),
}
const mapStateToProps = state => {
return (
({
list: state.resource.experiences,
languages: state.filter.languages
})
)
}
const mapDispatchToProps = (dispatch) => ({
readList: () => {
return (
dispatch(resourceListReadRequest('experiences', { apiRequestParams }))
)
}
})
export default connect(mapStateToProps, mapDispatchToProps)(ExperienceListContainer)
The problem with this current code is that mDTP cannot access apiRequestParams. It is undefined as far as it is concerned.
Based on my research, the answers that pop up are to use mergeProps (as an arg passed into connect()) and ownProps (a param included with mapDispatchToProps), but neither of these seem to be working, at least in the way I'm implementing them. Is there a better way to accomplish what I'm trying to do?
You have to pass them to readList as a parameter to access them like this:
readList(apiParams)
And use them like this:
readList: apiRequestParams => {
return (
dispatch(resourceListReadRequest('experiences', {
apiRequestParams }))
)
This is because apiRequestParams is local to the container itself and cannot be access outside of the component.
You would also have to declare it above the useEffect and pass it as parameter to the useEffect so that it gets evaluates, if the language changes for example and keep the reference with useRef or declare it within the useEffect so that you don't cause an infinity loop.
Hope that helps. Happy coding.
Related
I have custom React hook which adding some scripts and add a variable to window object:
const useMyHook = ({ id, type }: Props) => {
useScript('https:domain/main.js');
useScript('https://domain/main2.js');
useEffect(() => {
window.mydata = { id: `${id}`, type: `${type}` };
}, [id]);
};
I am using Apollo client and GraphQl for fetching data.
In my Page component, when I console.log(myData) the data returns undefined and then right after it returns the data (without refreshing). I am using Fragments.
From useQuery hook I can get the loading variable. How do I have to use loading and my custom hook so when loading === false -> use my custom hook.
I tried something like this:
const foo = useMyHook({ id: myData.id, type: myData.type });
Then below in the component in the return:
return (
{!loading && foo}
// Rest of the component jsx code
)
But still sometimes it returns first undefined?
How can I fix this issue?
# Update:
For now I added another prop loading: boolean and added this to the custom hook:
useEffect(() => {
if (!loading) {
window.mydata = { id: `${id}`, type: `${type}` };
}
}, [id]);
Is this correct approach. And why does my fragment query first returns undefined?
You can add another prop like enabled for handling this issue
sth like this:
useMyHook.tsx :
const useMyHook = ({ id, type,enabled }: Props) => {
useScript('https:domain/main.js');
useScript('https://domain/main2.js');
useEffect(() => {
if(enabled)
{
window.mydata = { id: `${id}`, type: `${type}` };
}
}, [enabled]);
};
other component:
const foo = useMyHook({ id: myData.id, type: myData.type,enabled:!loading && (data || error) });
return foo
and you need to use data and error that can be deconstructed from useQuery (like loading ) to just make sure if loading is false and data or error exists (that's because of first time that the component renders, loading is false but and data and error doesn't exists because request is not submitted yet, that's why you see undefined first )
I want to modify the state of a child component in React from a parent component a couple levels above it.
The child component is a react-table with pagination.
My use case is changing the data in the table with some client-side JS filtering.
The problem is, the table uses internal state to keep track of which page is being shown, and does not fully update in response to my filtering.
It is smart enough to know how much data it contains, but not smart enough to update the page it is on.
So, it might correctly say "Showing items 21-30 of 85", and then the user filters the data down to only four total items, and the table will say "Showing items 21-30 of 4".
I tried implementing something like what the FAQ suggests for manual state control, but that caused its own problem.
I was passing the new page index in as a prop, and that did set the page correctly, but it broke the ability for the user to navigate between pages, because any changes were immediately overwritten by the value of the prop.
Those instructions seem to work for a situation where all page index control gets handled by the parent, but not when some control should still be retained by the pagination mechanism.
I think what I need is an exposed function that lets me modify the value of the table's state.pageIndex as a one-off instead of passing a permanent prop. Is there a way to do that? Or any other way to solve my underlying problem?
Code follows. I apologize in advance I couldn't make this a real SSCCE, it was just too complicated, I tried to at least follow the spirit of SSCCEs as much as I could.
My page that lists stuff for the user looks like this:
// ...
const [searchTerms, setSearchTerms] = useState<Array<string>>([]);
// ...
const handleFilterRequestFromUser = function (searchTerms): void {
// ...
setSearchTerms(processedSearchTerms);
};
// ...
const visibleData = useMemo(() => {
// ...
}, [searchTerms]);
// ...
return (
<div>
// ...
<ImmediateParentOfTable
id={"Results"}
visibleData={visibleData} // User actions can affect the size of this
// ...
>
// ...
</div>
);
export default ListDatabaseResults;
Here's ImmediateParentOfTable:
import { Table, Pagination } from "#my-company/react";
// ...
return (
<Table
id={id}
pagination={{
render: (
dataSize,
{
pageCount,
pageOptions,
// ...
}
) => (
<Pagination
dataSize={dataSize}
pageCount={pageCount}
pageOptions={pageOptions}
gotoPage={gotoPage}
previousPage={previousPage}
nextPage={nextPage}
setPageSize={setPageSize}
canPreviousPage={canPreviousPage}
canNextPage={canNextPage}
pageIndex={pageIndex}
pageSize={pageSize}
pageSizeOptions={[10, 20, 50, 100]}
/>
),
manual: {
onPageChange: ({
pageIndex,
pageSize,
}: {
pageIndex: number;
pageSize: number;
}) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
},
rowCount,
pageCount: tablePageCount,
},
isLoading: !!dataLoading,
}}
/>
);
The custom Table inside #my-company/react (already in use in other places, so, difficult to modify):
import {
CellProps,
Column,
Hooks,
Row,
SortingRule,
TableState,
useFlexLayout,
usePagination,
UsePaginationInstanceProps,
UsePaginationState,
useRowSelect,
useSortBy,
useTable,
} from 'react-table';
// ...
export interface TableProps<D extends Record<string, unknown>> {
id: string;
// ...
pagination?: Pagination<D>;
pageIndexOverride?: number; // This is the new prop I added that breaks pagination
}
const Table = <D extends Record<string, unknown>>({
id,
columns,
data,
// ...
pageIndexOverride,
}: TableProps<D>): JSX.Element => {
const {
state: { pageIndex, pageSize, sortBy },
// ...
} = useTable(
{
columns,
data,
autoResetPage,
initialState,
useControlledState: (state) => {
return React.useMemo(
() => ({
...state,
pageIndex: pageIndexOverride || state.pageIndex, // This always resets page index to the prop value, so changes from the pagination bar no longer work
}),
[state],
);
},
// ...
I've encountered a similar problem with react-table where most of my functionality (pagination, sorting, filtering) is done server-side and of course when a filter is changed I must set the pageIndex back to 0 to rectify the same problem you have mentioned.
Unfortunately, as you have discovered, controlled state in v7 of react-table is both poorly documented and apparently just completely non-functional.
I will note that the example code you linked from the docs
const [controlledPageIndex, setControlledPage] = React.useState(0)
useTable({
useControlledState: state => {
return React.useMemo(
() => ({
...state,
pageIndex: controlledPageIndex,
}),
[state, controlledPageIndex]
)
},
})
is actually invalid. controlledPageIndex cannot be used as a dep in that useMemo because it is in the outer scope and is accessed through closure. Mutating it will do nothing, which is actually noted by eslint react/exhaustive-deps rule so it's quite surprising that this made it into the docs as a way of accomplishing things. There are more reasons why it is unusable, but the point is that you can forget using useControlledState for anything.
My suggestion is to use the stateReducer table option and dispatch a custom action that will do what you need it to. The table reducer actions can have arbitrary payloads so you can do pretty much whatever you want. ajkl2533 in the github issues used this approach for row selection (https://github.com/TanStack/react-table/issues/3142#issuecomment-822482864)
const reducer = (newState, action) => {
if (action.type === 'deselectAllRows') {
return { ...newState, selectedRowIds: {} };
}
return newState;
}
...
const { dispatch, ... } = useTable({ stateReducer: reducer }, ...);
const handleDeselectAll = () => {
dispatch({ type: 'deselectAllRows' });
}
It will require getting access to the dispatch from the useTable hook though.
I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea
What I'm trying to achieve:
I have a NextJS + Shopify storefront API application. Initially I set up a Context api for the state management but it's not that efficient because it re-renders everything what's wrapped in it. Thus, I'm moving all state to the Redux Toolkit.
Redux logic is pretty complex and I don't know all the pitfalls yet. But so far I encounter couple problems. For example in my old Context API structure I have couple functions that take a couple arguments:
const removeFromCheckout = async (checkoutId, lineItemIdsToRemove) => {
client.checkout.removeLineItems(checkoutId, lineItemIdsToRemove).then((checkout) => {
setCheckout(checkout);
localStorage.setItem('checkout', checkoutId);
});
}
const updateLineItem = async (item, quantity) => {
const checkoutId = checkout.id;
const lineItemsToUpdate = [
{id: item.id, quantity: parseInt(quantity, 10)}
];
client.checkout.updateLineItems(checkoutId, lineItemsToUpdate).then((checkout) => {
setCheckout(checkout);
});
}
One argument (checkoutId) from the state and another one (lineItemIdsToRemove) extracted through the map() method.
Inside actual component in JSX it looks and evokes like this:
<motion.button
className="underline cursor-pointer font-extralight"
onClick={() => {removeFromCheckout(checkout.id, item.id)}}
>
How can I declare this type of functions inside createSlice({ }) ?
Because the only type of arguments reducers inside createSlice can take are (state, action).
And also is it possible to have several useSelector() calls inside one file?
I have two different 'Slice' files imported to the component:
const {toggle} = useSelector((state) => state.toggle);
const {checkout} = useSelector((state) => state.checkout);
and only the {checkout} gives me this error:
TypeError: Cannot destructure property 'checkout' of 'Object(...)(...)' as it is undefined.
Thank you for you're attention, hope someone can shad the light on this one.
You can use the prepare notation for that:
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (id: number, text: string) => {
return { payload: { id, text } }
},
},
},
})
dispatch(todosSlice.actions.addTodo(5, "test"))
But 99% of the cases you would probably stay with the one-parameter notation and just pass an object as payload, like
dispatch(todosSlice.actions.addTodo({ id: 5, text: "test"}))
as that just works out of the box without the prepare notation and makes your code more readable anyways.
I'm using mobx-state-tree and mobx-react-lite, can someone guide me to a better pattern,
wishlist.js - wishlist store
import { types } from 'mobx-state-tree'
export const WishListItem = types.model('WishListItem', {
name: types.string,
price: types.number,
image: "",
}).actions(self => ({
changeName(newName) {
self.name = newName
},
}))
export const WishList = types.model('WishList', {
items: types.optional(types.array(WishListItem), []),
})
root.js - root store
export const RootStore = types.model('RootStore', {
counter: types.optional(Counter, { count: 0 }),
wishList: types.optional(WishList, {
items: [{ image: '', price: 10, name: 'Yoda' }]
}),
})
I'm updating the store as
setInterval(() => store.wishList.items[0].changePrice(Math.random() * 100), 500)
In my Wishlist view
wishlist.jsx
const WishListItem = ({ image, name, price }) => {
return useObserver(
() =>
<div>
<img src={image} />
<h3>{name}</h3>
<h5>{price}</h5>
</div>
)
}
const WishListView = ({ items }) => {
return useObserver(
() => <>
{
items.map(
(item, key) => <WishListItem {...item} key={key} />
)
}
</>
)
}
export default () => useObserver(() => (
<WishListView items={store.wishList.items} />
))
Here I have to use useObserver or Observer at every level of the component tree, to make it reactive, is there any way to pass a reactive reference to the child?
It works perfectly fine with primitive types like string or number, but with an array or an object, I have to either directly refer changing variables at the parent like store.wishList[0].price or use useObserver in the whole tree.
I want to pass the items array to children, and update children on the changes, just this at the root
export default () => useObserver(() => (
<WishListView items={store.wishList.items} />
))
and no more useObserver at it's childrens
Update
A workaround I found was to destructure the array, now the changes are reactive since we are directly accessing the variables that are changing.
export default () => useObserver(() => {
const items = store.wishList.items.map(item => ({ ...item }))
return <WishListView items={items} />
})
and no more useObserver at it's childrens
It is actually better to mark all components as observer if possible. For example, if you mark each Item as observer and one of the items change its name then only this component will rerender. If you dont make Item observer then your whole List will rerender which is quite bad if have lots of items or deep DOM tree. Also it does not make sense to rerender whole list when just one item changes.
Look here for explanation https://mobx.js.org/refguide/observer-component.html#when-to-apply-observer
So your workaround is a bad pratice and should be used only as last resort if you dont have control over children components and can't make them observer.