So I have a problem with state and localStorage. To be exact, whenever I nest the objects inside of the themes.dark and themes.light the results in dynamic codeblock differ after refresh even though the localStorage data is in fact correct.
import { useState, useEffect } from "react";
import store from "store2";
import "./styles.css";
const themes = {
dark: {
theme: "dark"
},
light: {
theme: "light"
}
};
export default function App() {
const defaultDarkTheme = window.matchMedia("(prefers-color-scheme:dark)")
.matches
? themes.dark
: themes.light;
const [theme, setTheme] = useState(store.get("theme") || defaultDarkTheme);
useEffect(() => {
store.set("theme", theme);
}, [theme]);
return (
<>
<div
className="App"
onClick={() =>
theme === themes.dark ? setTheme(themes.light) : setTheme(themes.dark)
}
>
{JSON.stringify(theme)}
</div>
{theme === themes.dark
? JSON.stringify(themes.dark)
: JSON.stringify(themes.light)}
</>
);
}
Before refresh :
After refresh :
It's pretty problematic since after refresh instead of generating the content for dark mode it generates the content for the light one.
I think it is because of your useEffect hook.
useEffect(() => {
store.set("theme", theme);
}, [theme]);
I think your useEffect runs before it gets the data from the localStorage. I'm not sure this is just a hunch.
Your problem is not with state or the storage. You will see your problem if you add the following line after declaring your state and refresh the page after changing it to dark.
console.log(theme, themes.dark, theme === themes.dark)
Javascript only decides that two objects are equal if they refer to the exact same object. However when you get your theme object from the store, it is not the exact same object as the themes.dark object you've defined earlier.
There are multiple ways to handle a scenario like this, but I think it would be simplest if you compare some string properties of the objects, for example,
theme.theme === themes.dark.theme
? JSON.stringify(themes.dark)
: JSON.stringify(themes.light)
alternatively you could define a unique identifier property on your theme object which you can use to compare instead of theme.theme or if you really want to compare the objects you could do something like
JSON.stringify(theme) === JSON.stringify(themes.dark)
? JSON.stringify(themes.dark)
: JSON.stringify(themes.light)
Related
I'm beginner with React testing, learning by coding, here i have a component 'cam.tsx'
i want to test it, when i want to test Add function it goes straight like this, but when i want to test Update function it still shows Add function in my test, how to test both of them ?
Add and Update functions are forms where user can fill.
describe("Testing component ", () => {
const Camera = (): RenderResult =>
render(
<Provider store={store}>
<Cam
}}
/>{" "}
</Provider>
);
test("Cam", () => {
Camera();
const name = screen.queryByTestId(/^AddName/i);
});
});
cam.tsx:
const ADD = "ADD";
let [state, setState] = useState<State>({mode: ADD });
if (props.mode) {
state.mode = props.mode;
}
const option = state.mode;
return (
<React.Fragment>
<div data-testid="header">
{option == ADD ? Add() : <></>}
{option == UPDATE ? Update() : <></>}
</div>
</React.Fragment>
Basically cam.tsx is a component which has two forms one for updating camera and another for adding new camera.When user clicks add/update icon then cam component gets 'mode' via props ' state.mode = props.mode '
English is not my mother language, so could be mistakes
Here is how to test a component that conditionally renders components from state and can be updated via props.
import {render, screen} from '#testing-library/react';
import {Cam} from './Cam';
test('renders add by default', () => {
render(<Cam/>);
expect(screen.getByTestId('addForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('updateForm'))
.not.toBeInTheDocument();
});
test('renders edit by passing props', () => {
const {rerender} = render(<Cam mode={undefined}/>);
rerender(<Cam mode={'UPDATE'} />)
expect(screen.getByTestId('updateForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('addForm'))
.not.toBeInTheDocument();
});
However, it is known in the React community that updating state via props is usually an anti-pattern. This is because you now have two sources of truth for state and can be easy to have these two states conflicting. You should instead just use props to manage rendering.
If state comes from a parent component, use props.
export function Cam(props) {
const option = props.mode;
return (
<div data-testid="header">
{option === ADD ? Add() : <></>}
{option === UPDATE ? Update() : <></>}
</div>
);
}
If you really want to keep state in the child component even if props are passed in, you should update props in an useEffect hook. Additionally, you should use the setState function rather than setting state manually state.mode = props.mode
Use the useEffect hook to update state via props.
...
const [state, setState] = useState({mode: ADD});
useEffect(() => {
if (props.mode) {
setState({mode: props.mode});
}
}, [props.mode]) <-- checks this value to prevent infinite loop.
const option = state.mode;
return (
...
I have a problem with reading {find}.
The problem is in ProductDetail.js.
First, click on the products link than on any product to see details.
TypeError: Cannot read properties of null (reading 'find')
https://codesandbox.io/s/react-router-product-detail-pages-dynamic-links-forked-y1o0n?file=/src/ProductDetail.js:418-429
You've done some mistakes over there in your ProductDetail.js file.
First:
You can use useEffect hook to check and compare if there is a matching id or not.
Second:
You can use useState hook to store the thisProduct and update the thisProduct value by calling setThisProduct and use it in the JSXElement.
This is always a best practice to use the state for data set and get.
Here is more about React.Hooks
Third:
Price is a Object and you can't render your object like that, so use the key instead of object while rendering. like this: {thisProduct?.price?.current?.value}
You can learn more about optional chaining
Fourth:
productId which you're getting from useParams is a string type, and your productId from sneakers is a number type. So you need to change your productId to number while comparing like this: Number(productId)
Learn about Numbers in Js
Here is the complete code of yours:
// ProductDetail.js
import React, { useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { StateContext } from "./GlobalContext";
function ProductDetail() {
const { productId } = useParams();
const { sneakers } = useContext(StateContext);
const [thisProduct, setThisProduct] = useState({});
useEffect(() => {
if (sneakers) {
const findProduct = sneakers.find((product) => {
return product.id === Number(productId);
});
console.log("findproduct", findProduct);
setThisProduct(findProduct);
}
}, [productId, sneakers]);
return (
<div>
{thisProduct && (
<>
<h1>{thisProduct?.name}</h1>
<p>Price: {thisProduct?.price?.current?.value}</p>
<p>{thisProduct?.description}</p>
</>
)}
</div>
);
}
export default ProductDetail;
completely check your state and props, it is not providing valid data to child component
<StateContext.Provider value={{ sneakers }}>
{console.log(sneakers, "== thisProduct")}
{children}
</StateContext.Provider>
console will show your data, it coming null so that is the issue
This is how my app looks like right now:
The search and sort functions are working dynamically, as soon as I type a letter, it finds the match but re-renders all of them.
If I type "hunger", it finds the hunger games films, but it's getting images and rendering when it already has them. Is there any way to make this process just for once so I don't wait for every search, every sorting? I use Redux so data is coming from store, store is getting the data from local json file.
I thought about storing on local storage but I couldn't figure it out. As you can see there is no ComponentDidMount or Hooks.
This is the code of this page:
import React from "react";
import "./MoviesAndSeries.css";
import ListShowItem from "./ListShowItem";
import { getVisibleShows } from "../Redux/Selector";
import { connect } from "react-redux";
import FilterShowItems from "./FilterShowItems";
const Movies: React.FC = (props: any) => {
return (
<div className="movies">
<FilterShowItems />
{props.movies.map((movie: any) => {
return <ListShowItem key={Math.random()} {...movie} />;
})}
</div>
);
};
const mapStateToProps = (state: any) => {
return {
movies: getVisibleShows(state.movies, state.filters),
filters: state.filters,
};
};
export default connect(mapStateToProps)(Movies);
You are using Math.random() as keys. The keys change all the time, and React can't know if the items already exist, so it re-renders all of them:
<ListShowItem key={Math.random()} {...movie} />;
Change the key to something stable, and unique, the movie id (if you have one) for example:
<ListShowItem key={movie.id} {...movie} />;
I have a bottomTabNavigator which has two stacknavigators. Each stacknavigator has their own respective screens within them. Whenever I use something like
navigator.navigate("Stackname" {screen:"screenname", randomProp: "seomthing")
the params are sent to the stacknavigator, and not the screen itself. I kinda got past the issue by passing in
initialParams=route.params
within the stacknavigators, but they won't refresh when I call the first block of code for a second time.
Any ideas?
Instead of:
navigator.navigate("StackName" {screen:"screenName", paramPropKey: "paramPropValue");
Use this:
navigator.navigate("screenName", {'paramPropKey': 'paramPropValue'});
In screenName:
export default ({route}) => {
useEffect(() => {
// do something
}, [route]);
};
That is because the screen is already mounted & initial params won't update. What you can do, though, is create a wrapper component enhanced with 'withNavigationFocus' that 'react-native-navigation' offers.
https://reactnavigation.org/docs/1.x/with-navigation-focus/
ComponentWithFocus
import React, {Component, useState} from 'react';
import { withNavigationFocus } from 'react-navigation';
const ComponentWithFocus = (props) => {
const {isFocused, onFocus, onBlur} = props;
const [focused, setFocused] = useState(false);
if(isFocused !== focused) {
if(isFocused) {
typeof onFocus === 'function' ? onFocus() : null;
setFocused(true)
} else {
typeof onBlur === 'function' ? onBlur() : null;
setFocused(false)
}
}
return (
props.children
)
}
export default withNavigationFocus(ComponentWithFocus);
And use it in your screen like this:
...
onFocus = () => {
//your param fetch here and data get/set
this.props.navigation.getParam('param')
//get
//set
}
...
render() {
<ComponentWithFocus onFocus={this.onFocus}>
/// Your regular view JSX
</ComponentWithFocus>
}
Note: If params aren't updated still, than you should reconsider your navigating approach. For example, there is no need to navigate from your tabBar like this:
navigator.navigate("Stackname" {screen:"screenname", randomProp: "seomthing")
You could instead do the following:
navigator.navigate("screenName", {'paramPropKey': 'paramPropValue'})
This will work because '.navigate' function finds the first available screen that matches the name and if it isn't already mounted it mounts it onto the stack (firing componentDidMount method). If the screen already exists, it just navigates to it, ignoring 'componentDidMount' but passing the 'isFocused' prop which, luckily, we hooked on to in our 'ComponentWithFocus'.
Hope this helps.
function HomeScreenComponent( {navigation} ) {
React.useEffect(() => {
navigation.addListener('focus', () => {
console.log("reloaded");
});
}, [navigation]);
export default HomeScreenComponent;
This will also listen to the focusing and execute the useEffect function when the screen navigates.
Edit. I rewrote the code to be even more minimalist. The below code is a spike test of my issue.
Here is a video of the issue:
https://imgur.com/a/WI2wHMl
I have two components.
The first component is named TextEditor (and it is a text editor) but its content is irrelevant - the component could be anything. A simple div with text would be just as relevant.
The next component is named Workflows and is used to render a collection from IndexDB (using the Dexie.js library). I named this collection "workflows" and the state variable I store them in is named workflows_list_array
What I am trying to do is the following:
When the page loads, I want to check if any workflows have a specific ID . If they do, I store them in workflows_list_array and render them. I don't need help with this part.
However, if no workflows with the aforementioned criteria exist, I want to keep the component named Workflows hidden and render the component named TextEditor. If workflows do exist, I want the TextEditor hidden and to display Workflows
The problem is that even though I have it working, when workflows do exist (when workflows_list_array is populated) the TextEditor "flickers" briefly before being hidden and then the Workflows component is displayed.
I can tell this is an async issue but I can't tell how to fix it.
I posted code below and I tried to keep it to a minimum.
Test.js
import React, {useState, useEffect} from "react";
import db from "../services"
function Workflows(props){
return (
<div>
<ul>
{
props.workflows.map((val,index)=>{
return <li key={index}>{val.content}</li>
})
}
</ul>
</div>
)
}
function TextEditor(){
return (
<div> TextEditor </div>
)
}
function Test(props){
let [workflows_list_array, set_state_of_workflows_list_array] = useState([]);
let [client_id_number, set_client_id_number] = useState(5);
useEffect(() => { // get all workflows of the selected client per its ID
db.workflows.toArray((workflows_list)=>{ // iterate through workflows array
return workflows_list
}).then((workflows_list)=>{
workflows_list.forEach((val)=>{
if(client_id_number === val.client_id){
set_state_of_workflows_list_array((prev)=>{
return [...prev, val]
});
}
});
});
}, []);
return(
<div>
{workflows_list_array.length ? null : <TextEditor/> }
{workflows_list_array.length ? <Workflows workflows={workflows_list_array}/> : null}
</div>
)
}
export default Test
services.js
import Dexie from 'dexie';
import 'dexie-observable';
var workflowMagicUserDB = new Dexie("WorkflowMagicUserDB");
workflowMagicUserDB.version(1).stores({
user: "",
workflows: "++id,client_id,content,title",
clients: "++id,name",
calendar_events: "++id,start,end,title"
});
export default workflowMagicUserDB
Why don't you include a flag which indicates if you have already got data from IndexDB, something like:
function Test(props){
const [loading, setLoading] = React.useState(true);
let [workflows_list_array, set_state_of_workflows_list_array] = useState([]);
let [client_id_number, set_client_id_number] = useState(5);
useEffect(() => {
db.workflows.toArray((workflows_list)=>{
}).then((workflows_list)=>{
}).finally(() => setLoading(false)); //when process finishes, it will update the state, at that moment it will render TextEditor or Workflows
}, []);
if(loading) return <LoadingIndicator/>; // or something else which indicates the app is fetching or processing data
return(
<div>
{workflows_list_array.length ? null : <TextEditor/> }
{workflows_list_array.length ? <Workflows workflows={workflows_list_array}/> : null}
</div>
)
}
export default Test
When the process finishes, finally will be executed and set loading state to false, after that, your app will render TextEditor or Workflows