Current behaviour
I'm using a functional component with a setState hook in useEffect. The state variable that is set inside useEffect is wrapped over the return statement to render the JSX for the component.
When I debug into it, the component renders with the correct state variable but my wrapper in my test does Not show the correct information.
wrapper.update() isn't fixing this issue.
Below is a snippet of what I am trying to achieve:
const DummyComponent= ({}) => {
const [selected, setSelected] = React.useState(false);
useEffect(() => {
setSelected(true)
}, [someDependency])
return (
{
selected && (
<div id= 'container'>
{childComponents}
</div>)
}
);
})
it('test', () => {
const wrapper= mount( <DummyComponent /> );
wrapper = wrapper.update(); // this doesn't fix my problem
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
I am getting the below error:
Method “props” is meant to be run on 1 node. 0 found instead.
Expected Behaviour
After state update in useEffect re-render should be triggered in test case and element with id="container" should be found.
Note: This is not same as https://github.com/enzymejs/enzyme/issues/2305
It seems to me there's some other problem with your real code (maybe some promise-based code invoked in the effect?). Here's a working example based on your snippet:
const DummyComponent = ({}) => {
const [selected, setSelected] = React.useState(false);
const [result, setResult] = React.useState("");
React.useEffect(() => {
setSelected(true);
}, []);
return selected && <div id='container' onClick={() => setResult("test")}>
<label>{result}</label>
</div>;
};
it('test', () => {
const wrapper = mount(<DummyComponent/>);
act(() => {
wrapper.find('#container').first().props().onClick();
});
expect(wrapper.find("label").text()).toEqual("test");
});
The act is actually needed only for interaction with the component itself, not for after-render effect.
The problem is that when you first mount the component, it does not render anything, because selected is false. So, when you search for '#container', you don't get anything.
If the update is enough, then it should probably be executed before the wrapper.find(), so that the component is rendered with selected true. But React is asynchronous and I suspect that this will not be enough…
I fixed my problem, actually I need to assign my component to a different wrapper and then update the wrapper and then check for updates on the wrapper instead of the component. Below is the snippet:
it('test', () => {
const component= mount( <DummyComponent /> );
const wrapper = component.update();
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
This will have the updated component
Related
I am trying to figure out how to make a searchFilter work. Here is my situation:
In App.js I hold a state for my items that i get from an api.
I also hold a state for the SearchFilter.
The items arrive and I can render them just fine.
Further, in App.js, I render the items and also a search component. So my code looks something like this:
const App = () => {
const [items, setItems] = useState([])
const [searchFilter, setSearchFilter] = useState("")
useEffect(() => {
const fetchItems = async () => {
// FETCHING ITEMS AND SETTING VIA setItems...
// This part works as expected
}
fetchItems()
},[])
return (
<>
<SearchBar setSearchFilter={setSearchFilter} />
<RenderItems items={items} searchFilter={searchFilter} />
</>
)
}
The problem I face is, that the searchFilter remains undefined in the RenderItems component. Why?
It gets updated correctly in App.js, but somehow doesn't make it's way to RenderItems
Inside component SearchBar:
const SearchBar = ({setSearchFilter}) => {
return (
<>
<input type="text" placeholder="Search" onChange={(e) => setSearchFilter(e.target.value) }/ >
</>
)
{
Any clues?
Thank you all for the replies #Mandeep Kaur and #KcH
I found the problem was in the data that came from the api when trying this scenario out in a codesandbox.
I keep the link here for future reference: https://codesandbox.io/s/nostalgic-booth-p1tqsv?file=/src/App.js
Closed from my side.
I think this happens because RenderItems component is not re-render after updating the state in SearchBar component.
You can try with adding one useEffect that makes it re-render and it gives the latest data to RenderItems
useEffect(() => {
},[searchFilter])
I am assuming that the updated value you getting in the App.js file.
Sometimes I have to use some native js libaray api, So I may have a component like this:
function App() {
const [state, setState] = useState(0)
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1.innerHTML = 'h1h1h1h1h1h1'
container.append(h1)
h1.onclick = () => {
console.log(state)
}
}, [])
return (
<div>
<button onClick={() => setState(state => state + 1)}>{state}</button>
<div id="container"></div>
</div>
)
}
Above is a simple example. I should init the lib after react is mounted, and bind some event handlers. And the problem is coming here: As the above shown, if I use useEffect() without state as the item in dependencies array, the value state in handler of onclick may never change. But if I add state to dependencies array, the effect function will execute every time once state changed. Above is a easy example, but the initialization of real library may be very expensive, so that way is out of the question.
Now I find 3 ways to reslove this, but none of them satisfy me.
Create a ref to keep state, and add a effect to change it current every time once state changed. (A extra variable and effect)
Like the first, but define a variable out of the function instead of a ref. (Some as the first)
Use class component. (Too many this)
So is there some resolutions that solve problems and makes code better?
I think you've summarised the options pretty well. There's only one option i'd like to add, which is that you could split your code up into one effect that initializes, and one effect that just changes the onclick. The initialization logic can run just once, and the onclick can run every render:
const [state, setState] = useState(0)
const h1Ref = useRef();
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1Ref.current = h1;
// Do expensive initialization logic here
}, [])
useEffect(() => {
// If you don't want to use a ref, you could also have the second effect query the dom to find the h1
h1ref.current.onClick = () => {
console.log(state);
}
}, [state]);
Also, you can simplify your option #1 a bit. You don't need to create a useEffect to change ref.current, you can just do that in the body of the component:
const [state, setState] = useState(0);
const ref = useRef();
ref.current = state;
useEffect(() => {
const container = document.querySelector('#container');
// ...
h1.onClick = () => {
console.log(ref.current);
}
}, []);
version
next: 12.0.7
suneditor: 2.41.3
suneditor-react: 3.3.1
const SunEditor = dynamic(() => import("suneditor-react"), {
ssr: false,
});
import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File
// states
const [toggle, setToggle] = useState<boolean>(false);
const [content, setContent] = useState<string>("");
useState(() => {
console.log(toggle, content);
}, [toggle, content]);
// render
return (
<SunEditor
onChange={(content) => {
setToggle(!toggle);
setContent(!content);
}
/>
);
When I type the console panel always shows me that only content was changed, but toggle is not changed.
I really don't know what happens, I just want to set other states inside SunEditor's onChange event, but I can't. Can anyone just explain to me how to fix this?
You can use a function inside the setToggle function to get the current state value and return the modified value for that state variable.
<SunEditor
onChange={(content) => {
setToggle((value) => !value);
setContent(content);
}
/>
Due to the async nature of setState, passing in a function into the state setter instead of a value will give you a reliable way to get the component’s state. It's the recommended approach when setting state based on previous state.
I'm working with Amazon's Chime SDK Component Library for React. The component library has a number of components and hooks available for use.
One of the component is <LocalVideo />. The problem is that it starts with the video disabled. In order to turn it on you use the hook useLocalVideo().
In the examples I've seen they use a button to execute the hook:
const MyComponent = () => {
const togglevideo = { useLocalVideo };
return(
<>
<LocalVideo />
<button onClick={toggleVideo}>Toggle</button>
</>
);
};
This works fine. But what if I want the LocalVideo component to load enabled? e.g., if I don't want someone to have to click a button?
I've tried several different approaches including:
Add the code directly to the component (this doesn't work, I assume because the hook is called before the render with the LocalVideo component completes).
Adding the code inside useEffect (invalid Hook call errors).
For example:
const MyComponent = () => {
useEffect( () => {
useLocalVideo();
},
);
const MyComponent = () => {
const { tileId, isVideoEnabled, setIsVideoEnabled, toggleVideo } = useLocalVideo();
useEffect( () => {
setIsVideoEnabled(true);
}, []
);
How can I make this hook run after the component renders?
You should call the function toggleVideo inside the useEffect, not the hook useLocalVideo itself:
const MyComponent = () => {
const { togglevideo } = useLocalVideo();
useEffect(() => {
togglevideo();
}, []);
return (
<LocalVideo />
);
};
I want to use a render props function with hooks but I am not entirely sure if this is possible.
I have a FetcherComponent that takes a renderprop but I want to use a setState from
export const HierarchyGraph: React.FunctionComponent = () => {
const [rootNode, setRootNode] = useState<HierarchyNode<GraphicalNode> | null>(null);
return (
<Fetcher
url="/hierarchy"
initialData={{}}
render={({ data }: { data: TreeData }) => {
// this will cause infinite recursion
setRootNode(getHierarchy(data));
Should I not use render props in this situation?
You can use a render prop but you have to branch inside the render function of HierarchyGraph to detect whether or not you have to make the call. Otherwise the request is triggered multiple times. Here is a quick example:
const HierarchyGraph = () => {
const [rootNode, setRootNode] = useState(null);
if (!rootNode) {
return (
<Fetcher
url="/hierarchy"
initialData={{}}
render={({ data }) => {
setRootNode(getHierarchy(data));
}}
/>
);
}
return <div>render the data related to rootNode</div>;
};
An alternative solution is to inline the call inside the render function and perform the operation on each render. It depends on the use case but if the operation is cheap it might be simpler. The last alternative is to leverage useEffect rather than the Fetcher component. Its usage would be more suited than the render prop pattern since you can explicitly trigger the call to the API only once.
it causes infinite recursion because of setRootNode causing re-render of HierarchyGraph and this again triggers setRootNode. You need to find a way to stop this state updates when it's not necessary e.g:
export const HierarchyGraph = () => {
const [data, setData] = useState({});
return (
<Fetcher
url="/hierarchy"
initialData={data}
render={({ data: newData }) => {
if(data !== newData) {
setData(newData);
}
}}
/>
);
}