I'm currently trying to implement a simple webapp with react and fluent ui. Right now I'm working on a toolbar with a Coachmark. But all that is not important. My function component make use of React.useState and initializes it from a property of type boolean. Directly after this the state is false, no matter what the value of that property is. Here is the relevant code snippet:
type HeadDataCommandBarProps = {
onSave: any,
onComment: any,
showCoachmark: boolean
};
const InvoiceHeadDataCommandBar: React.FunctionComponent<InvoiceHeadDataCommandBarProps> = ({ onSave, onComment, showCoachmark }) => {
console.log('showCoachmark', showCoachmark);
const [isCoachmarkVisible, setIsCoachmarkVisible] = React.useState(showCoachmark);
console.log('isCoachmarkVisible', isCoachmarkVisible);
const onDismissCoachmark = () => setIsCoachmarkVisible(false);
return (
<IndividualCommandBarButtonAsExample
onDismissCoachmark={onDismissCoachmark}
isCoachmarkVisible={isCoachmarkVisible}
onSave={onSave}
onComment={onComment}
/>
);
}
export default InvoiceHeadDataCommandBar;
This is the log output:
showCoachmark true InvoiceHeadDataCommandBar.tsx:94:12
isCoachmarkVisible false InvoiceHeadDataCommandBar.tsx:96:10
Could anybody point me in the right direction?
Instead of this:
isCoachmarkVisible={showCoachmark}
I would do this:
isCoachmarkVisible={isCoachmarkVisible}
Initial state is the value of the prop. Then you set a new state with the function. But what is having the new state is isCoachmarkVisible. Props are inmutable.
Related
I have a context for the layout of my React app that utilizes a hook for finding out the current window size:
export const LayoutContext = createContext({
menuIsOpen: false,
setMenuIsOpen: (isOpen: boolean) => {},
overlayIsOpen: false,
setOverlayIsOpen: (isOpen: boolean) => {},
isMobile: false,
isTablet: false,
isLaptop: false,
isDesktop: false,
});
export default function LayoutProvider({
children,
}: {
children: React.ReactNode;
}) {
const context = useLayoutContext();
return (
<LayoutContext.Provider value={context}>{children}</LayoutContext.Provider>
);
}
function useLayoutContext() {
const windowSize = useWindowSize();
const isMobile = windowSize.width <= Breakpoint.MEDIUM; // <= 768
...
...
// Initial load of page layout. If <=1024px neither chat or menu is open by default.
useEffect(() => {
const isMobileDevice = window.innerWidth <= Breakpoint.MEDIUM;
const isLargeDevice = window.innerWidth >= Breakpoint.LARGE;
const isDesktopDevice = window.innerWidth >= Breakpoint.EXTRA_LARGE;
if (isMobileDevice) {
setShowMobileNavigation(true);
}
if (isDesktopDevice) {
setMenuIsOpen(true);
}
}, []);
There's a bit more code but these are the important parts. I'm looking to replace this with a global Zustand store, but I'm having trouble understand exactly how to do it. Where would I use the useWindowSize hook? And where would I use the initial useEffect to decide the layout, if I'm to move away from this context provider that wraps the layout?
I know this isn't specifically a Zustand question but I figured the logic is roughly the same whether it be moving from Context to Redux, jotai, recoil...
Appreciate any and all help
In my case, Zustand store does not need to wrap the component and can use anywhere so im using a hook similar to useWindowSize and useEffect to detect device type in my App.tsx.
I know it's not a good pattern to do that, but you will understand why I want to do like that.
I have a HTable, which use a third-party library (react-table)
const HTable = <T extends object>({ columns, data, tableInstance}: Props<T>) {
const instance: TableInstance<T> = useTable<T> (
// Parameters
)
React.useImperativeHandle(tableInstance, () => instance);
}
Now, I want to control columns visibility from parent. I did:
const Parent = () => {
const [tableInstance, setTableInstance] = React.useState<TableInstance<SaleItem>>();
<Table data={data} columns={columns} tableInstance={(instance) => setTableInstance(instance)}
return tableInstance.columns.map((column) => {
<Toggle active={column.isVisible} onClick={() =>column.toggleHiden()}
}
}
The column hides well, but the state doesn't update and neither does the toggle, and I don't understand why. Could you help me to understand?
EDIT:
Adding a sandbox.
https://codesandbox.io/s/react-table-imperative-ref-forked-dilx3?file=/src/App.js
Please note that I cannot use React.forwardRef, because I use typescript and React.forwardRef doesn't allow generic type like this if I use forwardRef
interface TableProps<T extends object> {
data: T[],
columns: Column<T>[],
tableInstance?: React.RefObject<TableInstance<T>>,
}
Your issue is that react-tables useTable() hook always returns the same object as instance wrapper (the ref never changes). So your parent, is re-setting tableInstance to the same object - which does not trigger an update. Actually most of the contained values are also memoized. To get it reactive grab the headerGroups property.
const {
headerGroups,
...otherProperties,
} = instance;
React.useImperativeHandle(
tableInstance,
() => ({ ...properties }), // select properties individually
[headerGroups, ...properties],
);
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
I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.
I was trying to update the state after user selects the dropdown, however, the selected option is never changed. See the gif -- https://recordit.co/KH2Pqn34bp.
I am confused that ideally, after using setFilterOptions to update state, it's supposed to re-render this component with a new value, but it doesn't happen. Could anyone help take a look? What am I missing here? Thanks a lot!
Example code on sandbox -- https://codesandbox.io/s/react-select-default-value-forked-1ybdk?file=/index.js
const SearchFilter = () => {
const [filterOptions, setFilterOptions] = useContext(SearchFilterContext);
let curSort = filterOptions['sortType'] || DEFAULT_SORT_OPTION;
const handleSortChange = (option) => {
setFilterOptions(previous => Object.assign(previous, { 'sortType': option }))
};
return (
<span className='filter-container'>
<Select options={SORT_TYPE_OPTIONS} value={curSort} onChange={handleSortChange}/>
</span>
);
};
Couple of problems in your code:
To set the default value of the Select component, you have written some unnecessary code. Instead, you could just use the defaultValue prop to set the default value of the Select component.
<Select
options={OPTIONS}
defaultValue={OPTIONS[0]}
onChange={handleSortChange}
/>
You are mutating the state directly. Object.assign(...) returns the target object. In your case, the target object is the previous state.
Instead of returning the new state object, you mutate the state directly and return the previous state object which prevents a re-render.
Using the spread-syntax, you can update the state correctly as shown below:
const handleSortChange = (option) => {
setFilterOptions({ ...filterOptions, sortType: option });
};
Following code fixes the above mentioned problem in your component:
const SearchFilter = () => {
const [filterOptions, setFilterOptions] = useState({});
const handleSortChange = (option) => {
setFilterOptions({ ...filterOptions, sortType: option });
};
return (
<span className="filter-container">
<Select
options={OPTIONS}
defaultValue={OPTIONS[0]}
onChange={handleSortChange}
/>
</span>
);
};
The reason you're not seeing a change is that this functional component will only re-render when it sees that your state changed (based on your sandbox script). At the moment when you use Object.assign(previous, { 'sortType': option}) you're changing the sortType property in the object but the object itself doesn't change and so the functional component doesn't see a change.
We can resolve this by using either Object.assign({}, previous, { 'sortType': option}) which will create a NEW object with the previous state attributes and the changed sortType (the first param to Object.assign is the object where the following object properties will get copied into. if we use an empty object that's the equivalent of a new object) or we can use a spread operator and replace it with ({...sortType, 'sortType': option}) which will also create a new object that the functional component will recognize as a changed state value.
const handleSortChange = (option) => {
setFilterOptions(previous => Object.assign({}, previous, { 'sortType': option }))
};
or
const handleSortChange = (option) => {
setFilterOptions(previous => ({...previous, 'sortType': option})
};
Keep in mind these are shallow object copies.