I have a simple React component that injects an instance of the Rich Text Editor, TinyMCE into any page.
It is working, but sometimes a bad prop value gets through and causes errors.
I was wondering, if there is a way to check if the values of planetId or planetDescriptor are either empty or null before anything else on the page loads.
I tried wrapping all the code in this:
if(props)
{
const App = (props) => { ... }
}
But that always throws this error:
ReferenceError: props is not defined
Is there a way to check for certain values in props before I finish loading the component?
thanks!
Here is the app:
const App = (props) => {
const [planetDescriptor, setPlanetDescriptorState] = useState(props.planetDescriptor || "Planet Descriptor...");
const [planetId, setPlanetIdState] = useState(props.planetId);
const [planet, setPlanetState] = useState(props.planet);
const [dataEditor, setDataEditor] = useState();
const handleEditorChange = (data, editor) => {
setDataEditor(data);
}
const updatePlanetDescriptor = (data) => {
const request = axios.put(`/Planet/${planetId}/planetDescriptor`);
}
return (
<Editor
id={planetId.toString()}
initialValue={planetDescriptor}
init={{
selector: ".planetDescriptor",
menubar: 'edit table help'
}}
value={dataEditor}
onEditorChange={handleEditorChange}
/>
)
}
export default App;
You had the right idea in the conditional. Just need to put it inside the component rather than wrapping the whole thing. What you can try is something similar to what the react docs for conditional rendering has for a sample. What this does is it check if the props = null / undefined and then returns or renders the error state. Else it returns the Editor.
if (!props) {
return <h1>error state</h1>
}
return <Editor></Editor>
You can't wrap the code in the way you tried as you are working with JSX, not plain javascript, so you can't use the if statement there.
I suggest using a ternary, like so:
const SomeParentComponent = () => {
const propsToPass = dataFetchOrWhatever;
return (
<>
{propsToPass.planetDescriptor && propsToPass.planetId ?
<App
planetDescriptor={propsToPass.planetDescriptor}
planetId={propsToPass.planetId}
anyOtherProps={???}
/> :
null
}
</>
)
};
This will conditionally render the App component, only if both of those props exist.
You can also use && to have the same effect:
... code omitted ...
{propsToPass.planetDescriptor && propsToPass.planetId &&
<App
planetDescriptor={propsToPass.planetDescriptor}
planetId={propsToPass.planetId}
anyOtherProps={???}
/>
}
... code omitted ...
Which approach you use is largely up to preference and codebase consistency.
Related
I am new to working with contexts and I'm just trying to start slow. I saw a thing about logging your Provider to test the value and I am getting a constant undefined value. I have moved them right next to each other in the code to see if that changes anything.
const PromptContext = createContext('test123');
function generateRecipe() {
<PromptContext.Provider value="hello">xxx</PromptContext.Provider>
console.log(PromptContext.Provider.value);
console.log("Generating recipe...");
}
}
Upon this function being called the log value is always undefined, no matter what is put in the value of the Provider. Any ideas on fixing this?
The end goal is to get the value of the provider into this consumer which is in a separate react file
<PromptContext.Consumer>
{test => (
<h1>{test.value}</h1>
)}
</PromptContext.Consumer>
Your provider should not be part of a function (in the way you have it listed, anyway). The provider, of course, WILL be a function, but you aren't going to just be including it inside functions in the same way you showed above. It's actually easier than that!
You want it like this:
export const PromptContext = createContext();
export const PromptContextProvider = ({children}) => {
// All of your logic here
const baseValue = 'hello';
return (
<PromptContext.Provider value={{baseValue}}>
{children}
</PromptContext.Provider>
)
}
You don't mix the provider with your end function like you're doing above.
Instead, in your index.js file, you'll wrap your component:
root.render(
<PromptContextProvider>
<App />
</PromptContextProvider>
)
And then you can access your baseValue by using const {baseValue} = useContext(PromptContext) in your components.
React Context uses the component hierarchy to make state broadly available.
You create a provider with a value and use that to wrap other components. Anything in the component tree under the provider can then access the context value using either Context.Consumer or the useContext() hook.
For example
const PromptContext = createContext('test123');
const App = () => (
<PromptContext.Provider value="hello">
<ChildWithConsumer />
<ChildWithHook />
</PromptContext.Provider>
);
const ChildWithConsumer = () => (
<PromptContext.Consumer>
{(prompt) => (
<p>The context says "{prompt}"</p>
)}
</PromptContext.Consumer>
);
const ChildWithHook = () => {
const prompt = useContext(PromptContext);
return (
<p>The context says "{prompt}"</p>
);
};
I'm reusing a couple of external components to create my custom Combobox in strapi app.
Values are received from server so I need to add options dynamically.
Currently there is the following code:
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected
}) {
const [value, setValue] = useState('');
const combo = (<Combobox label="Country" value={value} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value="{entry.id}">{entry.name}</ComboboxOption>
);
})}
</Combobox>);
// setValue(valueSelected)
return combo;
}
And everything goes good until I try so set 'selected' option basing on another set of data. In static world I could just say useState(valueSelected) and it will work. But as code generated dynamically, there is no related option yet, so I get failure like "Failed to get 'props' property of undefined".
I tried to put this combobox into a variable and set state between creation and returning it (commented setValue line before the return statement) but then app gets in a loop and returns "Too many re-renders".
Does anyone has an idea of how to change/rewrite this to be able to set selected value for dynamically created combobox?
So I assume that the values are dynamically fetched and passed to the ComboboxCustom.
I think you can add setValue(valueSelected) inside an useEffect.
onChange of the prop valueSelected.something like,
useEffect(() => {
setValue(valueSelected)
}, [valueSelected])
Also handle the return when the value is not yet loaded. like before doing valuesList.map, first check if valueList ? (render actual) : (render empty)
Hope this helps!!
Thanks,
Anu
Finally I got working solution based on answer from #Anu.
Cause valuesList is got as GET-request from another hook, I have to check values are already present (first hook hit gives [] yet) and bind Combobox state updating to change of valuesList also. Though I don't fell like this solution is perfect.
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected,
}) {
const [value, setValue] = useState('');
let combo = null;
useEffect(() => {
if(combo && combo?.props?.children?.length > 0 && valuesList.length > 0) {
setValue(valueSelected)
}
}, [valueSelected, valuesList])
combo = (<Combobox label="Country" value={value?.toString()} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>);
return combo;
}
After that I decided avoid creating custom component based on already published as I'll need to add and process event listeners that are added for us in the existing components. So I placed this code directly into my modal and it also works:
const [countries, setCountries] = useState([]);
const [deliveryCountryValue, setDeliveryCountryValue] = useState('');
useEffect(async () => {
const countriesReceived = await countryRequests.getAllCountries();
setCountries(countriesReceived);
}, []);
useEffect(() => {
// If there is no selected value yet, set the one we get from order from server
const valueDelivery = deliveryCountryValue != '' ? deliveryCountryValue : order.country?.id;
if(countries.length > 0) {
setDeliveryCountryValue(valueDelivery);
order.country = countries.find(x => x.id == valueDelivery);
}
}, [deliveryCountryValue, countries])
<Combobox key='delivery-combo' label="Country" value={deliveryCountryValue?.toString()} onChange={setDeliveryCountryValue}>
{countries.map((entry) => {
return(
<ComboboxOption key={'delivery'+entry.id} value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>
I have a section with a fixed height. I don't know when the component mounts (first renders) whether the content coming in will fit or not. If it does NOT fit, then I need to render a 'Read More' button.
It looks like this:
I wrote this originally as a Class component using the lifecycle methods DidMount/DidUpdate:
Class Component
import React, { createRef } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
class ParagraphList extends React.Component {
state = {
overflowActive: false,
}
wrapper = createRef() // so we can get a ref to the height container
isOverflowing(el) {
if (el) return el.offsetHeight < el.scrollHeight
}
componentDidMount() {
this.setState({ overflowActive: this.isOverflowing(this.wrapper.current) })
}
componentDidUpdate() {
if (this.wrapper.current && !this.state.overflowActive) {
this.setState({
overflowActive: this.isOverflowing(this.wrapper.current),
})
}
}
handleClick() {
this.setState({ overflowActive: false })
}
render() {
const { moreButtonText, titleText, paragraphs, theme } = this.props
return (
<>
<Section overflowActive={this.state.overflowActive}>
{this.state.overflowActive || !this.wrapper.current ? (
<StyledHeightContainer ref={this.wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(this)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
}
export default ParagraphList
My best way to explain the flow:
When the component mounts, the flag is false and we have no reference to the div so the StyledHeightContainer will try to render and thus provide a ref to it
In componentDidMount -> try to set the overflow flag (which will be false because at this point we do not yet have rendering completed so the ref will be null). But by setting the flag anyway, we queue an additional render pass
1st INITIAL rendering completes -> we have a ref to the div now
The 2nd (queued) render occurs, firing the componentDidUpdate -> we can calculate the overflow and set the flag to true when the content overflows
When the user clicks the button -> set the flag to false, which will trigger a re-render and hence the StyledHeightContainer will be removed from the DOM.
Functional Component With Hooks
Sandbox of the code
When I re-wrote this as a functional component using Hooks, I ended up with this:
import React, { createRef, useEffect, useState } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
const ParagraphList = ({ moreButtonText, titleText, paragraphs, theme }) => {
const [overflowActive, setOverflowActive] = useState(false)
const [userClicked, setUserClicked] = useState(false)
const wrapper = createRef(false) // so we can get a ref to the height container
const isOverflowing = el => {
if (el) return el.offsetHeight < el.scrollHeight
}
useEffect(() => {
if (!userClicked && !overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
}, [userClicked]) // note: we only care about state change if user clicks 'Read More' button
const handleClick = () => {
setOverflowActive(false)
setUserClicked(true)
}
return (
<>
<Section theme={theme} overflowActive={overflowActive}>
{!userClicked && (overflowActive || !wrapper.current) ? (
<StyledHeightContainer ref={wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(null)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
export default ParagraphList
I was surprised that I needed to add another state (userClicked), which is how I force the 2nd render to occur (ie. the equivalent to the componentDidUpdate in the class solution).
Is this correct or can someone see a more concise way to write the 2nd solution?
NOTE
One of the reasons I ask is because in the console I get this warning:
48:6 warning React Hook useEffect has missing dependencies:
'overflowActive' and 'wrapper'. Either include them or remove the
dependency array react-hooks/exhaustive-deps
and I don't THINK I want to add them to the dependency array, as I don't want to trigger rendering when they change...?
I really enjoyed while solving the query.
Here is the implementation: https://codesandbox.io/s/react-using-hooks-in-section-component-5gibi?file=/src/ParagraphList.js
First of all, I was thinking of
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, [wrapper]);
But if we do this, it will again call the useEffect as when we'll click on the Read more button. Because it was comparing the reference of the wrapper and not it's value.
So, to avoid the reference comparison we have to use the useCallback hook.
const isOverflowingNode = node => {
return node.offsetHeight < node.scrollHeight;
};
const wrapper = useCallback(node => {
if (node !== null) {
setOverflowActive(isOverflowingNode(node));
}
}, []);
I came across the beautiful discussion: https://github.com/facebook/react/issues/14387
For more information:
https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
Thanks for the question :)
You could add an extra useEffect(() => (...),[]) that acts like componentDidMount(). And another useEffect(() => (...)) that acts like componentDidUpdate(). Then you should be able to get rid of userClicked.
This is a good link on how the lifestyle methods work with hooks. https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, []);
useEffect(() => {
if (!overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
});
The second one might need to be useLayoutEffect if you are wanting the update to happen after the layout.
I have some components that are rendering another component (FetchNextPageButton) that is already tested in isolation, like these ones:
const News = () => (
<div>
<h1>News</h1>
...
<FetchNextPageButton query={NEWS_QUERY} path="viewer.news" />
</div>
)
const Jobs = () => (
<div>
<h1>Jobs</h1>
...
<FetchNextPageButton query={JOBS_QUERY} path="viewer.jobs" />
</div>
)
const Posts = () => (
<div>
<h1>Posts</h1>
...
<FetchNextPageButton query={POSTS_QUERY} path="viewer.posts" />
</div>
)
The thing is that I'd not like having to add tests on each of these components for a functionality that is already tested somewhere else, so I think that should be enough just to test that the component is rendered and that I'm passing the right props to it.
I'd have been able to test this easily with Enzyme with something like this:
expect(wrapper.find('FetchNextPageButton').props()).toMatchObject({
query: NEWS_QUERY,
path: "viewer.news"
})
So I'm wondering what's the best approach to test it by using React testing library instead.
This is the approach that Kent C. Dodds (the creator of RTL) shared with me after discussing it with him:
import FetchNextPageButton from 'FetchNextPageButton'
jest.mock('FetchNextPageButton', () => {
return jest.fn(() => null)
})
// ... in your test
expect(FetchNextPageButton).toHaveBeenCalledWith(props, context)
Don't believe it's possible. RTL looks like focusing on validating against DOM not React's components tree.
The only workaround I see is to mock FetchNextPageButton to make it rendering all props into attributes.
jest.mock("../../../FetchNextPageButton.js", () =>
(props) => <div data-test-id="FetchNextPageButton" {...props} />);
....
const { getByTestId } = render(<YourComponent />);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("query", NEWS_QUERY);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("path", "viewer.news");
Sure, this is smoothly only for primitive values in props, but validating something like object or function would be harder.
Think, it's not RTL-way, but I agree it would be massive work to check that in scope of each container(and completely ignoring that would be rather a risk).
PS toHaveAttribute is from jest-dom
In my case, I wanted to test that a Higher Order Component (HOC), correctly enhances the component that is passed to the HOC.
What I needed to do, is make the actual component a mock and pass it to the HOC. Like described in the existing answer, you can then just expect the properties, added by the HOC.
// after 'Component' get's passed into withSelectionConstraint, it should have an id prop
const Component = jest.fn(() => <h1>Tag Zam</h1>);
const WithConstraint = withSelectionConstraint(Component, ["instance"], true);
render(<WithConstraint />);
// passing the jest mock to the HOC, enables asserting the actual properties passed by the HOC
expect(Component).toHaveBeenCalledWith(
expect.objectContaining({ ids: mockInstanceRows.map(x => x.id) }),
expect.anything()
)
Based on Ben's answer, I wrote a version which doesn't raise any error :
jest.mock(
'path/to/your/component',
() => {
const MockedComponent = (props: any) => {
const cleanedProps = Object.keys(props).reduce<Record<string, unknown>>(
(a, b) => {
// Needed because html attributes cannot be camel cased
a[b.toLowerCase()] = props[b].toString();
return a;
},
{}
);
return (
<div data-testid="any-test-id" {...cleanedProps} />
);
};
return MockedComponent;
}
);
Note that the attributes values (expect(getByTestId('any-test-id')).toHaveAttribute('attribute','value')) will be stringified.
This may be more a javascript question than a react-native/meteor question: I am adding Meteor connectivity to an existing React Native app, and have run into a snag with navigation. I previously had a ListView that provided an onPress function each row that would call the navigation. In keeping with Meteor's createContainer protocol, I've used (in my case) a "PuzzlesContainer" in place of the ListView that, in a separate file, refers to
const PuzzlesContainer = ({ puzzlesReady }) => {
return (
<Puzzles
puzzlesReady={puzzlesReady}
/>
);
};
export default createContainer(() => {
const handle = Meteor.subscribe('puzzles-list');
return {
puzzlesReady: handle.ready(),
};
}, PuzzlesContainer);
This file includes the "Puzzles" file, which is also a const function that contains the MeteorListView:
const Puzzles = ({ puzzlesReady }) => {
if (!puzzlesReady) {
return null;//<Loading />;
}else{
return (
<View style={launcherStyle.container}>
<MeteorListView
collection="puzzles"
renderRow={
(puzzle) =>
<View >
<TouchableHighlight style={launcherStyle.launcher} onPress={()=>onSelect(puzzle.text)}>
<Text style={launcherStyle.text}>{puzzle.text}</Text>
</TouchableHighlight>
</View>
. . .
My problem is that there is now no context for the original routing scheme, so when I call
this.props.navigator.push
it gives "undefined is not an object (evaluating 'this.props.navigator')". How can I handle this?
One way is to look at the new NavigationExperimental, which handles nagivator in a redux fashion.
Another method is, even though I do not know if this is recommended or not, to globalize the navigator component by assigning it to a module. It can be something like this
// nav.js
let nav = null
export function setNav = _nav => nav = _nav
export function getNav = () => {
if (nav) {
return nav
} else {
throw "Nav not initialized error"
}
}
Then when you first get hold of your navigator, do this
// component.js
import { Navigator } from 'react-native'
import { setNav } from './nav'
// ...
renderScene={ (route, navigator) => {
setNav(navigator)
// render scene below
// ...
}}
As much as I liked the suggestion of globalizing my navigation, a) I never managed to do it and b) it seemed like maybe not the best practice. For anyone else who might encounter this issue, I finally succeeded by passing the navigation props in each of the JSX tags--so:
<PuzzlesContainer
navigator={this.props.navigator}
id={'puzzle contents'}
/>
in the parent (react component) file, then
<Puzzles
puzzlesReady={puzzlesReady}
navigator={navigator}
id={'puzzle contents'}
/>
in the second 'const' (Meteor container) file, and using it
<TouchableHighlight onPress={()=>navigator.replace({id: 'puzzle launcher', ... })}>
in the third 'const' (MeteorListView) file. Hope it helps someone!