React context not updated in props.children - reactjs

I have a requirement where I need to show a date range picker and the selected date range should be made available to all the components. I'm trying to use React Context API to get this to work.
Using React.useContext() I am able to see the updated value in all components except whatever is rendered in {props.children}
Below is the approach I've tried out.
Context.tsx
export const DateRangeContext = React.createContext<DateRangeContextType>({
dateRange: {
startDate: new Date(),
endDate: new Date(),
},
setDateRange: (dateRange: DateRange) => {}
});
DateRangePicker.tsx
...
const { dateRange, setDateRange } = React.useContext(DateRangeContext);
...
const applyDateRange = (newDateRange: DateRange) => {
const newDateRange = {
startDate: newDateRange.startDate,
endDate: newDateRange.endDate,
};
setDateRange(newDateRange);
};
PageLayout.tsx
...
<DateRangeContext.Provider value={{dateRange, setDateRange}}>
<Header/>
{props.children}
<Footer/>
</DateRangeContext.Provider>
Dashboard.tsx
...
const { dateRange, setDateRange } = React.useContext(DateRangeContext);
...
<PageLayout>
<Card>Card content goes here</Card>
</PageLayout>
...
In PageLayout.tsx, if I directly use <Dashboard/> in place of {props.children} I see the updated context value in Dashboard.tsx.
But when Dashboard component is passed to {props.children}, the context value doesn't get updated and I always see the initialized value.

Not sure why but it worked after moving the provider to the PageLayout component's parent.
MainLayout.tsx
...
const [dateRange, setDateRange] = React.useState<DateRange>();
...
<DateRangeContext.Provider value={{dateRange, setDateRange}}>
<PageLayout/>
</DateRangeContext.Provider>

Related

How do you rerender a React component when an object's property is updated?

I have an object in my React application which is getting updated from another system. I'd like to display the properties of this object in a component, such that it updates in real time.
The object is tracked in my state manager, Jotai. I know that Jotai will only re-render my component when the actual object itself changes, not its properties. I'm not sure if that is possible.
Here is a sample that demonstrates my issue:
import React from "react";
import { Provider, atom, useAtom } from "jotai";
const myObject = { number: 0 };
const myObjectAtom = atom(myObject);
const myObjectPropertyAtom = atom((get) => {
const obj = get(myObjectAtom)
return obj.number
});
const ObjectDisplay = () => {
const [myObject] = useAtom(myObjectAtom);
const [myObjectProperty] = useAtom(myObjectPropertyAtom);
const forceUpdate = React.useState()[1].bind(null, {});
return (
<div>
{/* This doesn't update when the object updates */}
<p>{myObject.number}</p>
{/* This doesn't seem to work at all. */}
<p>{myObjectProperty}</p>
{/* I know you shouldn't do this, its just for demo */}
<button onClick={forceUpdate}>Force Update</button>
</div>
);
};
const App = () => {
// Update the object's property
setInterval(() => {
myObject.number += 0.1;
}, 100);
return (
<Provider>
<ObjectDisplay />
</Provider>
);
};
export default App;
Sandbox
you can use useEffect for this.
useEffect(()=> {
// code
}, [myObject.number])

React state not updating props (using next.js)

Trying to update the state of a react-date-range object (next.js) in a component with useState / setState. Not sure what I'm doing wrong, but I keep getting an error that dateRange.startDate is undefined.
import {useState, React} from 'react'
import { DefinedRange } from 'react-date-range'
import PropTypes from 'prop-types'
export const DataCard = ({...props}) => {
let [dateRange, setDateRange] = useState([{
startDate: props.dateRange.startDate,
endDate: props.dateRange.endDate,
key: 'selection'
}])
const updateDateRange = (dateRange) => {
props.dateRange = {startDate: dateRange.startDate, endDate: dateRange.endDate}
setDateRange([props.dateRange.startDate, props.dateRange.endDate])
}
return (
<div>
<div>
<button>
<DefinedRange onChange={item=> {updateDateRange([item.selection])}} ranges={dateRange}/>
</button>
</div>
</div>
)}
DataCard.propTypes = {
header: PropTypes.string,
dateRangeComponent: PropTypes.element,
dateRange: PropTypes.object
}
DataCard.defaultProps = {
header: "Graph Name",
dateRange: {startDate: new Date(), endDate: new Date()}
}
Your dispatch code is a bit problematic.
const updateDateRange = (dateRange) => {
console.log(dataRange)
props.dateRange = {startDate: dateRange.startDate, endDate: dateRange.endDate}
setDateRange([props.dateRange.startDate, props.dateRange.endDate])
}
I suspect two things therefore i suggest to add a console.log.
Possibility 1
Maybe your input isn't an array, instead it's a e from event handler. So first need to confirm that by printing it onto screen.
Possibility 2
When you change something, you'd like to take changes from your input, not prop. So you need to think of why you are using props in the dispatch. Props are coming from parent, not children. Use local variable instead.
Bonus
Normally we don't render stuff inside a <button> element. It might be irrelevant to your problem, but might complicate your other parts of work. Use <div> instead.
Your initial status is an array of 1 object but when you setDateRange, you provide an array of two objects
Your init data:
[{
startDate: props.dateRange.startDate,
endDate: props.dateRange.endDate,
key: 'selection'
}] // an array of a object
setDateRange([props.dateRange.startDate, props.dateRange.endDate]) // setDateRange([a, b]), array of two objects
It should be:
setDateRange((prevState) => ({ startDate: props.startDate, endDate: props.endDate, key: prevState.key }));

React Context and useCallback API refresh in child component best practice

I am using the Context API to load categories from an API. This data is needed in many components, so it's suitable to use context for this task.
The categories can be expanded in one of the child components, by using a form. I would like to be able to tell useCategoryLoader to reload once a new category gets submitted by one of the child components. What is the best practice in this scenario? I couldn't really find anything on google with the weird setup that I have.
I tried to use a state in CategoryStore, that holds a boolean refresh State which gets passed as Prop to the callback and can be modified by the child components. But this resulted in a ton of requests.
This is my custom hook useCategoryLoader.ts to load the data:
import { useCallback } from 'react'
import useAsyncLoader from '../useAsyncLoader'
import { Category } from '../types'
interface Props {
date: string
}
interface Response {
error?: Error
loading?: boolean
categories?: Array<Category>
}
const useCategoryLoader = (date : Props): Response => {
const { data: categories, error, loading } = useAsyncLoader(
// #ts-ignore
useCallback(() => {
return *APICALL with modified date*.then(data => data)
}, [date])
)
return {
error,
loading,
categories
}
}
export default useCategoryLoader
As you can see I am using useCallback to modify the API call when input changes. useAsyncloaderis basically a useEffect API call.
Now this is categoryContext.tsx:
import React, { createContext, FC } from 'react'
import { useCategoryLoader } from '../api'
import { Category } from '../types'
// ================================================================================================
const defaultCategories: Array<Category> = []
export const CategoryContext = createContext({
loading: false,
categories: defaultCategories
})
// ================================================================================================
const CategoryStore: FC = ({ children }) => {
const { loading, categories } = useCategoryLoader({date})
return (
<CategoryContext.Provider
value={{
loading,
topics
}}
>
{children}
</CategoryContext.Provider>
)
}
export default CategoryStore
I'm not sure where the variable date comes from in CategoryStore. I'm assuming that this is an incomplete attempt to force refreshes based on a timestamp? So let's complete it.
We'll add a reload property to the context.
export const CategoryContext = createContext({
loading: false,
categories: defaultCategories,
reload: () => {},
})
We'll add a state which stores a date timestamp to the CategoryStore and create a reload function which sets the date to the current timestamp, which should cause the loader to refresh its data.
const CategoryStore: FC = ({ children }) => {
const [date, setDate] = useState(Date.now().toString());
const { loading = true, categories = [] } = useCategoryLoader({ date });
const reload = useCallback(() => setDate(Date.now.toString()), []);
return (
<CategoryContext.Provider
value={{
loading,
categories,
reload
}}
>
{children}
</CategoryContext.Provider>
)
}
I think that should work. The part that I am most iffy about is how to properly memoize a function that depends on Date.now().

React functional component using the `useState` hook does not update onChange using ReactiveSearch

I am trying to use the ReactiveSearch component library to build a basic search application, and need to use the components as controlled component (https://reactjs.org/docs/forms.html). For all of the other filters I am working with, this is no problem, and the app detects changes and updates accordingly. However, for this DateRange component, it won't work. My working hypothesis is that it has something to do with the state value being an object rather than an array, but I can't find evidence to support that yet.
I've also tried using a regular class component, with the same result.
Link to Sandbox: https://codesandbox.io/s/ecstatic-ride-bly6r?fontsize=14&hidenavigation=1&theme=dark
Basic code snippet with no other filters
import React, { useState } from "react";
import {
ReactiveBase,
ResultsList,
DateRange,
SelectedFilters
} from "#appbaseio/reactivesearch";
const App = props => {
const [filterState, setFilterState] = useState({
DateFilter: { start: new Date(), end: new Date() }
});
return (
<div className="App">
<ReactiveBase
app="good-books-ds"
credentials="nY6NNTZZ6:27b76b9f-18ea-456c-bc5e-3a5263ebc63d"
>
<DateRange
value={filterState.DateFilter}
onChange={value => {
setFilterState({
...filterState,
DateFilter: {
start: value.start,
end: value.end
}
});
}}
componentId="DateFilter"
dataField="timestamp"
/>
<SelectedFilters />
</ReactiveBase>
</div>
);
};
export default App;
Just changing value to defaultValue worked for me (https://codesandbox.io/s/jolly-spence-1o8bv).
<DateRange
defaultValue={filterState.DateFilter}
onChange={value => {
setFilterState({
DateFilter: {
start: value.start,
end: value.end
}
});
}}
componentId="DateFilter"
dataField="timestamp"
/>
I also removed the DateFilter spread in your setFilterState, since your previous state was being fully overwritten regardless.
It turned out to be an underlying problem with how the ReactiveSearch library was comparing the dates, as well as not setting values properly. Will make a PR to fix it.

How to correctly mock React Navigation's getParam method using Jest

I have a React Native app in which I'm trying to write some integration tests using Jest & Enzyme. My situation is as follows, I have a component which fetches a navigation param being passed to it from the previous screen using getParam - which works fine normally, I'm just struggling to successfully get a value in there using mock data. My code looks like this:
In my container I have this in the render method:
const tickets = navigation.getParam('tickets', null);
Then in my test I have the following:
const createTestProps = (testProps: Object, navProps: any = {}) =>
({
navigation: {
navigate: jest.fn(),
getParam: jest.fn(),
...navProps,
},
...testProps,
} as any);
let props = createTestProps(
{},
{
state: {
// Mock navigation params
params: {
tickets: [
{
cellNumber: '123456789',
ticketId: 'xxx',
},
{
cellNumber: '123456789',
ticketId: 'xxx',
},
],
},
},
}
);
const container = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeProvider theme={theme}>
<TicketSummaryScreen {...props} />
</ThemeProvider>
</MockedProvider>
);
As you can see I've attempted to mock the actual navigation state, which I've checked against what's actually being used in the real component, and it's basically the same. The value for tickets is still undefined each time I run the test. I'm guessing it has to do with how I've mocked the getParam function.
Anyone have any ideas? Would be much appreciated!
I just successfully fixed this problem on my project. The only advantage that I had is that I had the render method being imported from a file I created. This is a great because all my tests can be fixed by just changing this file. I just had to merge some mocked props into the component that render was receiving.
Here's what it looked like before:
/myproject/jest/index.js
export { render } from 'react-native-testing-library'
After the fix:
import React from 'react'
import { render as jestRender } from 'react-native-testing-library'
const navigation = {
navigate: Function.prototype,
setParams: Function.prototype,
dispatch: Function.prototype,
getParam: (param, defaultValue) => {
return defaultValue
},
}
export function render(Component) {
const NewComponent = React.cloneElement(Component, { navigation })
return jestRender(NewComponent)
}
This setup is great! It just saved me many refactoring hours and probably will save me more in the future.
Maybe try returning the mock data from getParam
Try bellow example code.
const parameters = () => {
return "your value"
}
.....
navigation: {
getParam: parameters,
... navProps
},
... testProps
});
Give it a try
const navState = { params: { tickets: 'Ticket1', password: 'test#1234' } }
const navigation = {
getParam: (key, val) => navState?.params[key] ?? val,
}
here navState values will be params that you are passing.

Resources