Unable to access props during React.cloneElement after upgrade to 17.0.2 - reactjs

I use React.cloneElement to override the default styles fr Text in react-native. Below is my implementation:
const React = require('react')
const { Platform, Text } = require('react-native')
const colorThemes = require('galarm-config').colorThemes
const defaultFontProperties = {
...Platform.select({
android: {
fontFamily: 'Roboto',
fontSize: 16
},
ios: { fontSize: 16 }
})
}
const oldRender = Text.render
if (oldRender) {
Text.render = function (...args) {
const origin = oldRender.call(this, ...args)
const colorScheme = origin.props.colorScheme
console.tron.log('text color scheme', colorScheme, origin.props.children)
return React.cloneElement(origin, {
style: [
defaultFontProperties,
{ color: colorThemes.getColorTheme(colorScheme).textColor },
origin.props.style
]
})
}
}
I have been using it successfully for quite some time. I recently updated to react-native 0.66 after which the below code stopped working. The issue is that the colorScheme prop is read as undefined on line 19.
Below is how the colorScheme is set
<Text colorScheme={'dark'}>
{'Some text'}
</Text>
I am not sure if something has changed in react 17.0.2 which causes the colorScheme prop to be not set. I have run it in debugger and of course the colorScheme prop is not set but I don't know how to debug this further.
Any help is appreciated.

Something should have changed in react/react-native internals that filters out custom props in the rendered node(origin). However, you can still access all the props you pass via the args since the first one corresponds to the props. You can do this instead
const props = args[0];
const colorScheme = props.colorScheme;
UPDATE
as for your second issue, try changing the order in the styles array to apply the overrides last
style: [
defaultFontProperties,
origin.props.style,
{ color: colorThemes.getColorTheme(colorScheme).textColor }
]
setting the color this way will force it even when you explicitly set the color when using the Text component, you may want to check if the style prop comes with a color and set that instead of the textColor from the theme
const propsStyle = props.style ? StyleSheet.flatten(props.style) : {};
...
{
color:
propsStyle.color || colorThemes.getColorTheme(colorScheme).textColor,
},

If, by colorScheme you mean (dark/light) mode, you can grab it directly from the appearance module:
import {Appearance } from 'react-native';
const theme = Appearance.getColorScheme();
so your code should work like this:
const React = require('react')
const { Platform, Text, Appearance } = require('react-native')
const colorThemes = require('galarm-config').colorThemes
const defaultFontProperties = {
...Platform.select({
android: {
fontFamily: 'Roboto',
fontSize: 16
},
ios: { fontSize: 16 }
})
}
const oldRender = Text.render
if (oldRender) {
Text.render = function (...args) {
const origin = oldRender.call(this, ...args)
const colorScheme = Appearance.getColorScheme(); // either 'light' or 'dark'
console.tron.log('text color scheme', colorScheme, origin.props.children)
return React.cloneElement(origin, {
style: [
defaultFontProperties,
{ color: colorThemes.getColorTheme(colorScheme).textColor },
origin.props.style
]
})
}
}
otherwise i don't know

Related

How can I use a variable value for class name when using makeStyles?

In my React app, I have a component which takes in some data. Depending on the value of that data, I want to show the component with a different coloured background.
The styles are defined as:
const useStyles = makeStyles((theme) => ({
class1: {
backgroundColor: "red",
},
class2: {
backgroundColor: "pink",
},
}));
The component is:
const MyBox = ({ data }) => {
let classes = useStyles();
let innerClassName;
if (data.value) {
innerClassName = "class1";
} else {
innerClassName = "class2";
}
return (
<div className={innerClassName}>
Content goes here
</div>
);
};
export default MyBox;
However, this gives the component a class of "class1" or "class2", which doesn't get picked up by makeStyles. I also tried <div className={classes.innerClassName}> but then it looks for a class called 'innerClassName' which obviously it can't find.
I think I need to use some kind of variable string within <div className={????}> but I've tried various template literal strings and none of them have worked. What should I be doing?

How to get the result 'toHaveStyle' in testing-library-react?

Testing library react does not catch 'toHaveStyle'.
When I clicked on the 'Content', its children which have a blue color were changed to the red color.
However, in my test, they always have the blue color.
What should I do to solve this problem?
[...]
<Content data-testid={"list-element-content"} id={data.id} toggle={state[data.id - 1]}>
<div>{data.titleUnBold}</div>
<BoldTitle>{data.titleBold}</BoldTitle>
</Content>
[...]
const Content = styled.div`
color: ${ (props) => props.toggle ? "red" : "blue" };
`;
Below the test code:
test("color changed", () => {
const mockState = [false];
const mockSwitchGuide = jest.fn();
const { getAllByTestId, rerender } = render(
<GuideListPresenter
data={mockListData}
state={mockState}
onClick={mockSwitchGuide}
/>
);
act(() => {
fireEvent.change(getAllByTestId("list-element-content")[0],{
target: {toggle: true},
});
});
rerender(
<GuideListPresenter data={mockListData} state={mockState} onClick={mockSwitchGuide} />
);
expect(getAllByTestId("list-element-content")[0].toggle).toEqual(true); // success
expect(getAllByTestId("list-element-content")[0]).toHaveStyle("color: red"); // failed
})
To test the style of your component, you can get it directly from the html document, and see precisely what style is used for a specific element.
In your example, you would do something like below:
it('should change color to red on toggle click', () => {
const { container, getAllByTestId } = render(
<GuideListPresenter
data={mockListData}
state={mockState}
onClick={mockSwitchGuide}
/>
);
// Replace <YOUR_DIV_ID> by your component's id
let contentDiv = document.getElementById('<YOUR_DIV_ID>');
let style = window.getComputedStyle(contentDiv[0]);
expect(style.color).toBe('blue'); // Sometimes, only rgb style type is read here. See the rgb that corresponds to your color if need be.
act(() => {
fireEvent.change(getAllByTestId("list-element-content")[0],{
target: {toggle: true},
});
});
// Get the updated contentDiv
contentDiv = document.getElementsByClassName('<YOUR_DIV_CLASS_NAME>');
style = window.getComputedStyle(contentDiv[0]);
expect(style.color).toBe('red');
expect(getAllByTestId("list-element-content")[0].toggle).toEqual(true);
}
Here, to get the style of your element, I am using the element's id. However, it could also work with the element's className, and using the method document.getElementByClassName('YOUR_DIV_CLASS_NAME') instead. Note that the given name here should be unique, either with the id technique, or the className.

Unable to use "this" in makeStyle

I would like to create a dynamic background image depending on my props. So for that I wanted to make a react style and give it the picture stored in my state but I can't use this.state.pictures in it and I don't know why.
class DisplayArtist extends Component {
state = {
name : this.props.Info.artists.items[0].name,
followers: this.props.Info.artists.items[0].followers.total,
genres: this.props.Info.artists.items[0].genres,
picture: this.props.Info.artists.items[0].images[0]
}
useStyles = makeStyles({
root: {
backgroundImage={this.state.pictures}
}
});
makeStyles is better used in a functional component, rather than a class component.
using makeStyes inside a function component causes the style to be recreated on every render. I don't recommend doing it that way.
The recommended approach is to use inline styles for dynamic background images
e.g. style={{ backgroundImage: artist.images[0] }}
Converting to Functional Component
const DisplayArtist = (props) => {
const [ artist, setArtist ] = useState(null);
useEffect(() => {
//do your own checks on the props here.
const { name, total, genres, images } = props.Info.artists.items[0]
setArtist({name, total, genres, images});
},[props])
return ( <div style={{ width: '200px', height:'200px', backgroundImage: artist.images[0] }} /> )
}
export default DisplayArtist
You can use Functional Component and use useStyles and pass the state you want to it
const useStyles = makeStyles(() => ({
root: {
backgroundImage:({ picture }) => ( picture )
}
}))
function DisplayArtist({Info}) {
const [picture, setPicture] = useState()
const classes = useStyles({picture})
}
Use Higher-order component API Higher-Order-Component-Api
const styles = theme => ({
root: {
backgroundImage={this.state.pictures}
}
});
class DisplayArtist extends Component {
state = {
name : this.props.Info.artists.items[0].name,
followers: this.props.Info.artists.items[0].followers.total,
genres: this.props.Info.artists.items[0].genres,
picture: this.props.Info.artists.items[0].images[0]
}
}
export default withStyles(styles)(DisplayArtist);

React.cloneElement clone already cloned element to add new props

I have have TestWrapper component that clones element and is supposed to make the background blue. Test is also doing the same, but instead sets color. I was expecting the rendered element to have blue background and red text, but it is only red. Here is the example:
const Test: React.FC<{ element: React.ReactElement }> = ({ element }) => {
return React.cloneElement(element, {
const isSelected = useIsSelected();
style: { ...element.props.style, color: isSelected ? 'red' : 'black' }
});
};
const TestWrapper: React.FC<{ element: React.ReactElement }> = ({
element
}) => {
// BackgroundColor is blue
const { backgroundColor } = useTheme();
return React.cloneElement(element, {
style: { ...element.props.style, background: backgroundColor }
});
};
export function App() {
return <TestWrapper element={<Test element={<h1>Heading</h1>} />} />;
}
How can I achieve this? I could do this differently, but I have to be able to access hook methods from Test and TestWrapper.
Simple codesandbox example: https://codesandbox.io/s/serene-bassi-ve1ym?file=/src/App.tsx
In TestWrapper you are cloning the Test component and applying your style props to it, which are not being passed down to the element that it's cloning itself. Just returning a cloned element doesn't create a referential equality where doing something to the component will affect the element it is cloning. You would need to give a style prop to Test and pass it down to the element being cloned:
const Test: React.FC<{
style?: React.CSSProperties;
element: React.ReactElement;
}> = ({ element, style }) => {
return React.cloneElement(element, {
style: { ...element.props.style, ...style, color: "red" }
});
};
I made a fork here. Hopefully this helps!

Remove (or at least hide) card on react-admin List

I want to get rid of the Card on the background react-admin's List (v3.4.2). I get the desired effect if I define a string on the component property:
<List component={"some string"}/>
But this spams the console with an error:
And I don't want to have that error. On top of that, I think I shouldn't be changing the component property (I can't find it on the official docs).
The code should be the following: https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/list/List.js
How should I do this? Is it possible to pass a style? Is there any component that works out of the box? Or should I just go custom?
You can hide the background using styling:
import { makeStyles } from '#material-ui/core/styles'
const useListStyles = makeStyles(theme => ({
content: {
boxShadow: 'none',
backgroundColor: 'inherit',
},
main: {
// backgroundColor: 'red',
},
root: {
// backgroundColor: 'red',
},
}))
const MyList = (props) => {
const classes = useListStyles()
return (
<List classes={classes} {...props} >
...
</List>
)
}

Resources