I used Jest and Enzyme to test the component and I ran into a problem with reloading. I want my component to retrieve the state from the localStorage. I didn't implement the code yet but the test still pass.
it('can preserve todo group', () => {
const wrapper = mount(<TodosContainer />);
wrapper.find('input[name="todo-container-form"]').simulate('change', { target: { value: 'Loy' } });
wrapper.find('button').simulate('click');
// eslint-disable-next-line no-undef
window.location.reload();
/* This is where it should fail without implementation
I didn't add any localStorage code in my component. */
expect(wrapper.find(TodosGroup).first().prop('name')).toMatch('Loy');
});
This is my component with irrevalent part redacted.
const TodosContainer = () => {
const [todosGroups, setTodosGroups] = useState([]);
const [groupName, setGroupName] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const addTodoGroups = (todoGroup: ITodoGroup) => {
setTodosGroups([...todosGroups, todoGroup]);
};
const changeName = (e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
};
const handleClick = () => {
if (groupName.length > 0) {
addTodoGroups({ name: groupName, key: Date.now() });
setGroupName('');
setErrorMessage('');
} else {
setErrorMessage('Group name must not be empty');
}
};
return (
<div >
<div >
{errorMessage.length > 0 ? <ErrorMessage css={css`width: 100%`}>{errorMessage}</ErrorMessage> : ''}
<TextInput name="todo-container-form" className="todoGroupName" value={groupName} onChange={changeName} />
<Button type="button" onClick={handleClick}>Add</Button>
</div>
{todosGroups.length > 0 ? todosGroups.map((todoGroup) => (
<div
key={todoGroup.key}
>
<TodosGroup name={todoGroup.name} />
</div>
)) : (
<p>
No todos group
</p>
)}
</div>
);
};
export default TodosContainer;
since jest uses JSDom instead of real browser window.location.reload(); does nothing. Also I'm afraid jsdom does not implement localStorage either(but it's easy to mock that)
You want to ensure that after component is recreated it takes state from localstorage, right?
This way(after appropriate mocking localStorage) you may just unmount component and mount it again:
wrapper.unmount();
const newOne = mount(<TodosContainer />);
expect(newOne.find(TodosGroup).first().prop('name')).toMatch('Loy');
Explicit unmounting is not required here, but I'd rather have it to simulate conditions completely(say, it would run componentWillUnmount/useEffect that may be part of logic).
Related
I am trying to create my own "vanilla-React" toast notification and I did manage to make it work however I cannot wrap my head around why one of the solutions that I tried is still not working.
So here we go, onFormSubmit() I want to run the code to get the notification. I excluded a bunch of the code to enhance readability:
const [notifications, setNotifications] = useState<string[]>([]);
const onFormSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
const newNotifications = notifications;
newNotifications.push("success");
console.log(newNotifications);
setNotifications(newNotifications);
};
return (
<>
{notifications.map((state, index) => {
console.log(index);
return (
<ToastNotification state={state} instance={index} key={index} />
);
})}
</>
</section>
);
Inside the Toast I have the following:
const ToastNotification = ({
state,
instance,
}:
{
state: string;
instance: number;
}) => {
const [showComponent, setShowComponent] = useState<boolean>(true);
const [notificationState, setNotificationState] = useState(
notificationStates.empty
);
console.log("here");
const doNotShow = () => {
setShowComponent(false);
};
useEffect(() => {
const keys = Object.keys(notificationStates);
const index = keys.findIndex((key) => state === key);
if (index !== -1) {
const prop = keys[index] as "danger" | "success";
setNotificationState(notificationStates[prop]);
}
console.log(state);
}, [state, instance]);
return (
<div className={`notification ${!showComponent && "display-none"}`}>
<div
className={`notification-content ${notificationState.notificationClass}`}
>
<p className="notification-content_text"> {notificationState.text} </p>
<div className="notification-content_close">
<CloseIcon color={notificationState.closeColor} onClick={doNotShow} />
</div>
</div>
</div>
);
};
Now for the specific question - I cannot understand why onFormSubmit() I just get a log with the array of strings and nothing happens - it does not even run once - the props get updated with every instance and that should trigger a render, the notifications are held into a state and even more so, should update.
What is wrong with my code?
I am experimenting with ReactJS custom Hooks, but I don't understand what's happening in the example below!
I expect to see on the screen: 'Label: <followed by one of the selected option ("Bananas" or "Apples" or "Oranges")>', but it is 'Label: ' so the optionis undefined!
Could someone explain me what's happening under the hood, why I cannot see the expected output for the option ?
const useFruit = () => {
const [option, setOption] = useState<string>();
const [options] = useState(["Bananas", "Apples", "Oranges"]);
return {
option,
setOption,
options,
};
};
const FruitDropdown = () => {
const { options, setOption } = useFruit();
return (
<select
placeholder="Select option"
onChange={(e) => {
setOption(e.target.value);
}}
>
{options.map((option) => (
<option value={option}>{option}</option>
))}
</select>
);
};
const FruitLabel = () => {
const { option } = useFruit();
return (
<label>Label: {option}</label>
);
};
export default function play() {
return (
<>
<FruitDropdown />
<FruitLabel />
</>
);
}
Just because they are using the same custom hook, they are not automatically sharing state. Every time you run useFruits you will create a new isolated state which is only accessable in that instance how the hook. And whenever a the state is created it defaults to undefined.
What you need in order to solve your problem is to wrap your components inside a context and place the state inside the context. Something like this:
const FruitContext = createContext()
const FruitProvider = ({ children }) => {
const [option, setOption] = useState<string>();
const [options] = useState(["Bananas", "Apples", "Oranges"]);
return (
<FruitContext.Provider value={{ option, setOption, options }}>{children}</FruitContext.Provider>
)
}
export const useFruits = () => useContext(FruitContext)
Dont forget to wrap your components:
<FruitProvider>
<FruitDropdown />
<FruitLabel />
</FruitProvider>
I'm building a simple toast notification system using React Context. Here is a link to a simplified but fully working example which shows the problem https://codesandbox.io/s/currying-dust-kw00n.
My page component is wrapped in a HOC to give me the ability to add, remove and removeAll toasts programatically inside of this page. The demo has a button to add a toast notification and a button to change the activeStep (imagine this is a multi-step form). When the activeStep is changed I want all toasts to be removed.
Initially I did this using the following...
useEffect(() => {
toastManager.removeAll();
}, [activeStep]);
...this worked as I expected, but there is a react-hooks/exhaustive-deps ESLint warning because toastManager is not in the dependency array. Adding toastManager to the array resulted in the toasts being removed as soon as they were added.
I thought I could have fixed that using useCallback...
const stableToastManager = useCallback(toastManager, []);
useEffect(() => {
stableToastManager.removeAll();
}, [activeStep, stableToastManager]);
...however, not only does this not work but I would rather fix the issue at the source so I don't need to do this every time I want this kind of functionality, as it is likely to be used in many places.
This is where I am stuck. I'm unsure as to how to change my Context so that I don't need add additional logic in the components that are being wrapped by the HOC.
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]);
const add = (content, options) => {
// We use the content as the id as it prevents the same toast
// being added multiple times
const toast = { content, id: content, ...options };
setToasts([...toasts, toast]);
};
const remove = id => {
const newToasts = toasts.filter(t => t.id !== id);
setToasts(newToasts);
};
const removeAll = () => {
if (toasts.length > 0) {
setToasts([]);
}
};
return (
<ToastContext.Provider value={{ add, remove, removeAll }}>
{children}
<div
style={{
position: `fixed`,
top: `10px`,
right: `10px`,
display: `flex`,
flexDirection: `column`
}}
>
{toasts.map(({ content, id, ...rest }) => {
return (
<button onClick={() => remove(id)} {...rest}>
{content}
</button>
);
})}
</div>
</ToastContext.Provider>
);
};
export const withToastManager = Component => props => {
return (
<ToastContext.Consumer>
{context => {
return <Component toastManager={context} {...props} />;
}}
</ToastContext.Consumer>
);
};
If you want to "Fix it from the core", you need to fix ToastProvider:
const add = useCallback((content, options) => {
const toast = { content, id: content, ...options };
setToasts(pToasts => [...pToasts, toast]);
}, []);
const remove = useCallback(id => {
setToasts(p => p.filter(t => t.id !== id));
}, []);
const removeAll = useCallback(() => {
setToasts(p => (p.length > 0 ? [] : p));
}, []);
const store = useMemo(() => ({ add, remove, removeAll }), [
add,
remove,
removeAll
]);
Then, the useEffect will work as expected, as the problem was that you re-initialized the ToastProvider functionality on every render when it needs to be a singleton.
useEffect(() => {
toastManager.removeAll();
}, [activeStep, toastManager]);
Moreover, I would recommend to add a custom hook feature as the default use case, and providing wrapper only for class components.
In other words, do not use wrapper (withToastManager) on functional components, use it for classes, as it is considered an anti-pattern, you got useContext for it, so your library should expose it.
// # toastContext.js
export const useToast = () => {
const context = useContext(ToastContext);
return context;
};
// # page.js
import React, { useState, useEffect } from 'react';
import { useToast } from './toastContext';
const Page = () => {
const [activeStep, setActiveStep] = useState(1);
const { removeAll, add } = useToast();
useEffect(() => {
removeAll();
}, [activeStep, removeAll]);
return (
<div>
<h1>Page {activeStep}</h1>
<button
onClick={() => {
add(`Toast at ${Date.now()}!`);
}}
>
Add Toast
</button>
<button
onClick={() => {
setActiveStep(activeStep + 1);
}}
>
Change Step
</button>
</div>
);
};
export default Page;
This question already has answers here:
Make React useEffect hook not run on initial render
(16 answers)
Closed last month.
I'm trying to use the useEffect hook inside a controlled form component to inform the parent component whenever the form content is changed by user and return the DTO of the form content. Here is my current attempt
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const onChange = ({target}) => {
console.log("onChange")
setValue(target.value)
}
return { value, setValue, binding: { value, onChange }}
}
useFormInput.propTypes = {
initialValue: PropTypes.any
}
const DummyForm = ({dummy, onChange}) => {
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [foo, bar])
return (
<div>
<input type="text" {...fooBinding} />
<div>{foo}</div>
<input type="text" {...barBinding} />
<div>{bar}</div>
</div>
)
}
function App() {
return (
<div className="App">
<header className="App-header">
<DummyForm dummy={{value: "Initial"}} onChange={(dummy) => console.log(dummy)} />
</header>
</div>
);
}
However, now the effect is ran on the first render, when the initial values are set during mount. How do I avoid that?
Here are the current logs of loading the page and subsequently editing both fields. I also wonder why I get that warning of missing dependency.
onChange callback
App.js:136 {foo: "Initial", bar: "Initial"}
backend.js:1 ./src/App.js
Line 118: React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
r # backend.js:1
printWarnings # webpackHotDevClient.js:120
handleWarnings # webpackHotDevClient.js:125
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage # webpackHotDevClient.js:190
push../node_modules/sockjs-client/lib/event/eventtarget.js.EventTarget.dispatchEvent # eventtarget.js:56
(anonymous) # main.js:282
push../node_modules/sockjs-client/lib/main.js.SockJS._transportMessage # main.js:280
push../node_modules/sockjs-client/lib/event/emitter.js.EventEmitter.emit # emitter.js:53
WebSocketTransport.ws.onmessage # websocket.js:36
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial"}
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial2"}
You can see this answer for an approach of how to ignore the initial render. This approach uses useRef to keep track of the first render.
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
} else {
// do things after first render
}
});
As for the warning you were getting:
React Hook useEffect has a missing dependency: 'onChange'
The trailing array in a hook invocation (useEffect(() => {}, [foo]) list the dependencies of the hook. This means if you are using a variable within the scope of the hook that can change based on changes to the component (say a property of the component) it needs to be listed there.
If you are looking for something like componentDidUpdate() without going through componentDidMount(), you can write a hook like:
export const useComponentDidMount = () => {
const ref = useRef();
useEffect(() => {
ref.current = true;
}, []);
return ref.current;
};
In your component you can use it like:
const isComponentMounted = useComponentDidMount();
useEffect(() => {
if(isComponentMounted) {
// Do something
}
}, [someValue])
In your case it will be:
const DummyForm = ({dummy, onChange}) => {
const isComponentMounted = useComponentDidMount();
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
if(isComponentMounted) {
console.log("onChange callback")
onChange({foo, bar})
}
}, [foo, bar])
return (
// code
)
}
Let me know if it helps.
I create a simple hook for this
https://stackblitz.com/edit/react-skip-first-render?file=index.js
It is based on paruchuri-p
const useSkipFirstRender = (fn, args) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
console.log('running')
return fn();
}
}, args)
useEffect(() => {
isMounted.current = true
}, [])
}
The first effect is the main one as if you were using it in your component. It will run, discover that isMounted isn't true and will just skip doing anything.
Then after the bottom useEffect is run, it will change the isMounted to true - thus when the component is forced into a re-render. It will allow the first useEffect to render normally.
It just makes a nice self-encapsulated re-usable hook. Obviously you can change the name, it's up to you.
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Example:
useEffectAfterMount(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [count])
I don't understand why you need a useEffect here in the first place. Your form inputs should almost certainly be controlled input components where the current value of the form is provided as a prop and the form simply provides an onChange handler. The current values of the form should be stored in <App>, otherwise how ever will you get access to the value of the form from somewhere else in your application?
const DummyForm = ({valueOne, updateOne, valueTwo, updateTwo}) => {
return (
<div>
<input type="text" value={valueOne} onChange={updateOne} />
<div>{valueOne}</div>
<input type="text" value={valueTwo} onChange={updateTwo} />
<div>{valueTwo}</div>
</div>
)
}
function App() {
const [inputOne, setInputOne] = useState("");
const [inputTwo, setInputTwo] = useState("");
return (
<div className="App">
<header className="App-header">
<DummyForm
valueOne={inputOne}
updateOne={(e) => {
setInputOne(e.target.value);
}}
valueTwo={inputTwo}
updateTwo={(e) => {
setInputTwo(e.target.value);
}}
/>
</header>
</div>
);
}
Much cleaner, simpler, flexible, utilizes standard React patterns, and no useEffect required.
First of all, my approach could just be misguided from the start.
I have a component that lists objects added by a sibling component.
I would like the list component to update when a new object is added.
As you can see, I'm calling the same function (getHostedServiceList) in both components. Obviously, this would need t be cleaned up, but I'd like to get it working first
I'm using hooks to accomplish this.
//input
const options = [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
];
// class Remotes extends Component {
const Remotes = ({ ...props }) => {
const [service, setService] = useState();
const [url, setUrl] = useState();
const [token, setToken] = useState();
const [displayName, setDisplayName] = useState();
const [apiUrl, setApiUrl] = useState();
const [services, setServices] = useState();
let HOME = process.env.HOME || '';
if (process.platform === 'win32') {
HOME = process.env.USERPROFILE || '';
}
const getHostedServiceList = () => {
console.log('props', props);
if (!fs.existsSync(`${HOME}/providers.json`)) {
return newMessage(
`Unable to locate ${HOME}/providers.json`,
'error',
);
}
const payload = JSON.parse(
fs.readFileSync(`${HOME}/providers.json`),
);
setServices(payload);
};
const setProvider = selectedOption => {
setService(selectedOption.value);
setUrl(`http://www.${selectedOption.value}.com`);
setApiUrl(`http://www.${selectedOption.value}.com/api/v1`);
};
const { onAddRemote } = props;
return (
<div>
<div>Add a remote host:</div>
<StyledSelect
value="Select Provider"
onChange={setProvider}
options={options}
/>
{console.log('service', service)}
<TextInput
label="Url"
defaultValue={url}
onChange={e => {
setProvider(e.target.value);
}}
disabled={!service ? 'disabled' : ''}
/>
<TextInput
label="API Url"
defaultValue={apiUrl}
onChange={e => setApiUrl(e.target.value)}
disabled={!service ? 'disabled' : ''}
/>
<TextInput
label="Token"
onChange={e => setToken(e.target.value)}
disabled={!service ? 'disabled' : ''}
/>
<TextInput
label="Display Name"
onChange={e => setDisplayName(e.target.value)}
disabled={!service ? 'disabled' : ''}
/>
<Button
disabled={!service || !url || !token}
onClick={() => {
onAddRemote({ service, url, apiUrl, token, displayName });
getHostedServiceList();
}}
>
Add Remote
</Button>
</div>
);
};
//list
const HostedProviderList = ({ ...props }) => {
const [services, setServices] = useState();
let HOME = process.env.HOME || '';
if (process.platform === 'win32') {
HOME = process.env.USERPROFILE || '';
}
const getHostedServiceList = () => {
console.log('props', props);
if (!fs.existsSync(`${HOME}/providers.json`)) {
return newMessage(
`Unable to locate ${HOME}/providers.json`,
'error',
);
}
const payload = JSON.parse(
fs.readFileSync(`${HOME}/providers.json`),
);
setServices(payload);
};
useEffect(() => {
// console.log('props 1', services);
getHostedServiceList();
}, []);
return (
<Wrapper>
<Flexbox>
<Title>Provider List</Title>
</Flexbox>
<div>
{services &&
services.map((service, i) => (
<Service key={i}>
<ServiceName>{service.displayName}</ServiceName>
<ServiceProvider>{service.service}</ServiceProvider>
</Service>
))}
</div>
</Wrapper>
);
};
I would like the list component to update when a new object is added.
Yes, you could use Redux (or React's own 'context') for global state handling. However, a simpler solution to be considered might just be to send the data to the parent and pass to the list component like so:
class Parent extends Component {
state = { objectsAdded: [] }
onObjectAdded = ( obj ) => {
// deepclone may be needed
this.setState({objectsAdded: this.state.objectsAdded.concat(obj)})
}
render() {
return (
<Fragment>
<ListComponent objects={this.state.objectsAdded} />
<ObjectAdder onObjectAdded={this.onObjectAdded} />
</Fragment>
}
}
This is where something like Redux or MobX comes in handy. These tools allow you to create a "store" - the place where you load and store data used by different components throughout your app. You then connect your store to individual components which interact with the data (displaying a list, displaying a create/edit form, etc). Whenever one component modifies the data, all other components will receive the updates automatically.
One way this cross-communication is accomplished is through a pub/sub mechanism - whenever one component creates or modifies data, it publishes (or "dispatches") an event. Other components subscribe (or "listen") for these events and react (or "re-render") accordingly. I will leave the research and implementation up to the reader as it cannot be quickly summarized in a StackOverflow answer.
You might also try the new React hooks, as this allows you to easily share data between components. If you choose this option, please take special care to do it properly as it is easy to be lazy and irresponsible.
To get you started, here are some articles to read. I highly recommend reading the first one:
https://reactjs.org/docs/thinking-in-react.html
https://redux.js.org/basics/usage-with-react
https://mobx.js.org/getting-started.html