useStyles is not dynamically assigning properties based on variable change - javascript

Based on a boolean state change, I'm trying to change the style of a component, but for some reason, it's only displaying the change after a page refresh.
In my parent App component, I do the following:
import React from "react";
import Layout from "./Layout";
export default function App() {
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => {
setLoading(!loading);
}, 2000);
}, [loading]);
return <Layout loading={loading} />;
}
And my Layout component catches this loading variable and send it to the makeStyles hook,
import React from "react";
import { makeStyles } from "#material-ui/core";
const useStyles = makeStyles((theme) => ({
root: {},
wrapper: ({ loading }) => ({
paddingTop: 64,
[theme.breakpoints.down("lg")]: {
paddingLeft: loading ? 0 : 100
}
})
}));
const Layout = React.memo(({ loading }) => {
const classes = useStyles({ loading });
return (
<div className={classes.root}>
<div>side</div>
<div className={classes.wrapper}>
is loading: {JSON.stringify(loading)}
</div>
</div>
);
});
export default Layout;
Doing a console.log after wrapper: ({ loading }) => ({ prints the correct value of loading, but the style is not changing.
What's going on here?

there is two issues in your code , the first one is that you use object destructuring two times :
instead of
const Layout = React.memo(({ loading }) => { const classes = useStyles({ loading });
you should :
const Layout = React.memo(({ loading }) => { const classes = useStyles(loading );
because in your first level you have access to the property loading, second issue is that you're invoking the parameter in the wrong place, you must invoke the loading directly in the css property, like this :
paddingLeft: (loading) => (loading ? 0 : 100)
here is a link with the two corrections ,hope that what you are expecting
https://codesandbox.io/s/goofy-microservice-y2gql?fontsize=14&hidenavigation=1&theme=dark

Related

how to write a test for below component in react?

could you please help me in writing the test for below component?
import { string, node } from "prop-types"
import * as Styled from "./Banner.styled"
const Banner = ({ heading, button }) => (
<Styled.Banner>
<Styled.Heading>{heading}</Styled.Heading>
{button && button}
</Styled.Banner>
)
Banner.propTypes = {
heading: string.isRequired,
button: node,
}
Banner.defaultProps = {
button: null,
}
export default Banner
I need to write a test in react library to see if the imported component(button) in rendering.
Could you please help me ? I tried the following but I think this is wrong :) The first test passes, but the second for the button itself is wrong:(
import { render, screen } from "../../../test-utils"
import Banner from "../Banner"
const heading = "heading"
const button = "button"
describe(`Banner`, () => {
it(`renders Banner with default properties`, () => {
render(<Banner heading={heading} />)
expect(screen.getByText(heading)).toBeInTheDocument()
})
it(`renders Banner with default properties`, () => {
render(<Banner button={button} />)
expect(screen.getByText(button)).toBeInTheDocument()
})
})
The second test case fails because your Banner expects heading props as required.
describe(`Banner`, () => {
it(`renders Banner without default properties`, () => {
render(<Banner heading={heading} />)
expect(screen.getByText(heading)).toBeInTheDocument()
})
it(`renders Banner with default properties`, () => {
render(<Banner heading={heading} button={button} />)
expect(screen.getByText(button)).toBeInTheDocument()
})
})
Try giving heading props in the second one.

Unable to re-render FlatList using extraData prop and State Variable

What I am trying to do
I am trying to get a set of songs play one after another. My app leverages Expo-Av API for playing the songs.
The app is consisted of two components. App.js and a ChildComponent.
App.js renders three songs using a flatlist. It also keeps track of which song has played using a state variable. The sequence of events as I intend them to happen are as follows:
Expected Steps
nextSongToPlay index is set to 0 when the app loads
Flatlist renders 3 ChildComponents using the Data array
Each ChildComponent is passed the index of renderItem as well as the nextSongToPlay
Within the first instance of ChildComponent, once the audio has been loaded using the loadAsync() function which returns a promise. If the promise is resolved AND the index (zero 0) and nextSongToPlay (zero 0 at first run) props are equal, the button within the ChildComponent is pressed by calling its reference (ref.current.props.onPress() )
Once the song finished playing the function passed to onPlayBackStatusUpdate method in ChildComponent is run (looking at the playBackObjectStatus.didJustFinish) and if the didJustFinish property is true, the NextSongToPlay method in App.js is called which has been passed to ChildComponent as a prop.
Once the NextSongToPlay method is run, the netSongToPlay state variable in App.js is incremented by 1.
This causes the re-render of App.js along with the FlatList. In
order to force the FlatList to re-render (since FlatList is a pure
component), the nextSongToPlay variable is passed to the extraData
prop within FlatList
The renderItem is ran again and this time the second ChildComponent will receive index (1) and nextSongToPlay (1). This will cause the loadAsync() method in second ChildComponent to call the ref.current.props.onPress() and play the song.
The process should continue until the last song in the Data array.
Here is what my App.js looks like:
import { View, Text, FlatList } from "react-native";
import React, { useState, useRef, useEffect } from "react";
import ChildComponent from "./ChildComponent";
const Data = [
{
key: "1",
song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav",
},
{
key: "2",
song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav",
},
{
key: "3",
song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav",
},
];
export default function App() {
const [nextSongToPlay, setNextSongToPlay] = useState(0);
const shouldPlayOnItsOwn = useRef(false);
useEffect(() => {
return () => (shouldPlayOnItsOwn.current = false);
});
const NextSongToPlay = () => {
setNextSongToPlay(nextSongToPlay + 1);
};
const setShouldPlayOnItsOwn = () => {
shouldPlayOnItsOwn.current = true;
};
const renderItem = ({ item, index }) => {
return (
<View style={{ marginTop: 10 }}>
<ChildComponent
path={item.path}
NextSongToPlay={() => NextSongToPlay()}
nextSongToPlay={nextSongToPlay}
index={index}
songURL={item.song}
setShouldPlayOnItsOwn={setShouldPlayOnItsOwn}
shouldPlayOnItsOwn={shouldPlayOnItsOwn.current}
/>
</View>
);
};
return (
<View
style={{ justifyContent: "center", alignItems: "center", marginTop: 200 }}
>
<FlatList
data={Data}
renderItem={renderItem}
extraData={nextSongToPlay}
/>
<Text style={{ marginTop: 30 }}>
{" "}
Number of Songs Played: {nextSongToPlay}{" "}
</Text>
</View>
);
}
And this is what my ChildComponent looks like:
import { View, Button } from "react-native";
import React, { useRef, useEffect } from "react";
import { Audio } from "expo-av";
export default function ChildComponent(props) {
const sound = useRef(new Audio.Sound());
const PlayBackStatus = useRef();
const ref = useRef();
const alreadyPlayed = useRef(false);
useEffect(() => {
LoadAudio();
return () => sound.current.unloadAsync();
}, []);
const LoadAudio = async () => {
PlayBackStatus.current = sound.current
.loadAsync({ uri: props.songURL })
.then((res) => {
console.log(`load result : ${res}`);
if (props.index === props.nextSongToPlay && props.shouldPlayOnItsOwn) {
ref.current.props.onPress();
}
})
.catch((err) => console.log(err));
};
const PlayAuido = async () => {
alreadyPlayed
? sound.current.replayAsync()
: (PlayBackStatus.current = sound.current
.playAsync()
.then(() =>
console.log(`result of playing: ${PlayBackStatus.current}`)
)
.catch((err) => console.log(`PlayAsync Failed ${err}`)));
};
sound.current.setOnPlaybackStatusUpdate((playBackObjectStatus) => {
console.log(
`Audio Finished Playing: ${playBackObjectStatus.didJustFinish}`
);
if (playBackObjectStatus.didJustFinish) {
console.log(
`Inside the If Condition, Did the Audio Finished Playing?: ${playBackObjectStatus.didJustFinish}`
);
alreadyPlayed.current = true;
props.NextSongToPlay();
}
});
const onPressHandler = () => {
PlayAuido();
props.setShouldPlayOnItsOwn();
};
return (
<View>
<Button title="Play Sound" onPress={onPressHandler} ref={ref} />
</View>
);
}
What is the Problem
Everything seems to work fine until step 7 in the expected steps section above. Even though the nextSongToPlay state variable does increment after the first song is played, the Flatlist doesnot seem to be getting rendered.
Here is the snack to reproduce this.
Any help in determining the issue is greatly appreciated!
Thanks in advance!

React render component only for few seconds

In my existing react component, I need to render another react component for a specific time period.
As soon as the parent component mounts/or data loads, the new-component (or child component) should be visible after 1-2 seconds and then after another few seconds, the new-component should be hidden. This needs to be done only if there is no data available.
This is what currently I've tried to achieve:
import React, { useState, useEffect } from "react";
function App() {
const [showComponent, setShowComponent] = useState(false);
const sampleData = [];
useEffect(() => {
if (sampleData.length === 0) {
setTimeout(() => {
setShowComponent(true);
}, 1000);
}
}, [sampleData]);
useEffect(() => {
setTimeout(() => {
setShowComponent(false);
}, 4000);
}, []);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
The current implementation is not working as expected. The new-component renders in a blink fashion.
Here is the working snippet attached:
Any help to resolve this is appreciated!
Every time App renders, you create a brand new sampleData array. It may be an empty array each time, but it's a different empty array. Since it's different, the useEffect needs to rerun every time, which means that after every render, you set a timeout to go off in 1 second and show the component.
If this is just a mock array that will never change, then move it outside of App so it's only created once:
const sampleData = [];
function App() {
// ...
}
Or, you can turn it into a state value:
function App() {
const [showComponent, setShowComponent] = useState(false);
const [sampleData, setSampleData] = useState([]);
// ...
}
I have modified the code to work, hope this how you are expecting it to work.
import React, { useState, useEffect } from "react";
const sampleData = [];
// this has to be out side or passed as a prop
/*
reason: when the component render (caued when calling setShowComponent)
a new reference is created for "sampleData", this cause the useEffect run every time the component re-renders,
resulting "<h2>found error</h2>" to flicker.
*/
function App() {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
if (sampleData.length === 0) {
const toRef = setTimeout(() => {
setShowComponent(true);
clearTimeout(toRef);
// it is good practice to clear the timeout (but I am not sure why)
}, 1000);
}
}, [sampleData]);
useEffect(() => {
if (showComponent) {
const toRef = setTimeout(() => {
setShowComponent(false);
clearTimeout(toRef);
}, 4000);
}
}, [showComponent]);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
You can try this for conditional rendering.
import { useEffect, useState } from "react";
import "./styles.css";
const LoadingComponent = () => <div>Loading...</div>;
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const onLoadEffect = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
setTimeout(() => {
setIsError(true);
}, 10000);
};
useEffect(onLoadEffect, []);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div className="App">
{isError ? (
<div style={{ color: "red" }}>Something went wrong</div>
) : (
<div>Data that you want to display</div>
)}
</div>
);
}
I needed to do imperatively control rendering an animation component and make it disappear a few seconds later. I ended up writing a very simple custom hook for this. Here's a link to a sandbox.
NOTE: this is not a full solution for the OP's exact use case. It simply abstracts a few key parts of the general problem:
Imperatively control a conditional render
Make the conditional "expire" after duration number of milliseconds.

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

How to persist data in react native app with usememo hook

I'm writing an application in react native and I came across a problem - the application will have several screens (I use react-navigation and react-navigation-tabs) and two-color themes (light and dark) managed by context and hooks. What I would like to achieve is the selected theme to be remembered by the app (the light theme will be set as default, and after switching to dark, leaving the application and returning the dark theme should still be applied).
EDIT #2: One answer from yesterday (that disappeared for some reason) suggested the use of redux and local storage so I'm editing the paragraph below to clarify the situation.
Easiest way would be to use sync storage/localStorage (I already have working version of the app using local storage), but one tutorial I found on the web uses the user memo hook for this purpose, and while it should work, it isn't (in my case at least), and I don't know why...
My App.js file below:
imports ...
const TabNavigator = createBottomTabNavigator({
Home: Home,
List: List,
});
const App = createAppContainer(TabNavigator);
export default () => (
<ThemeProvider>
<App />
</ThemeProvider>
ThemeContext.js file:
imports ...
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = useState(themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
And Home.js file, with a button to switch between themes:
imports ...
export const Home = () => {
const { colors, setColors } = useContext(ThemeContext);
const toggleTheme = () => {
if (colors.type === 'light') {
setColors(themes.darkTheme);
} else {
setColors(themes.lightTheme);
}
}
return (
<>
<View style={{...styles.mainView, backgroundColor: colors.backgroundColor }}>
<Text style={{...styles.mainText, color: colors.color}}>Hello Native World</Text>
<Button title='toggle theme' onPress={toggleTheme} />
</View>
</>
)
}
const styles = StyleSheet.create({
mainView: {
paddingTop: 40,
display: 'flex',
alignItems: 'center',
},
mainText: {
fontSize: 40,
fontWeight: 'bold',
},
});
The key file you have to change is your context file:
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = useState(themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
I don't really understand why you use useMemo, but I will leave it. From a quick glance I'd say that it is not needed, but I don't know your app. What you want is something like this:
import AsyncStorage from '#react-community/async-storage'
export const ThemeContext = createContext()
export function usePersistedState(key, initialState) {
const [state, setState] = useState(() => {})
useEffect(() => {
async function getAndSetInitialState() {
const persistedState = await AsyncStorage.getItem(key)
if (persistedState) {
setState(JSON.parse(persistedState))
} else if (typeof initialState === 'function') {
return setState(initialState())
} else {
return setState(initialState)
}
}
getAndSetInitialState()
}, [key])
function setPersistedState(value) {
AsyncStorage.setItem(key, JSON.stringify(value))
setState(value)
}
return [state, setPersistedState]
}
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = usePersistedState("your_storage_key", themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors]
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
I might have missed some edge cases, but this way your app will load it's state from the storage and save it's state into the storage.
EDIT: I'm not sure how useMemo would help, AsyncStorage is the easiest solution imo.

Categories

Resources