setting state from a different component - javascript

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

Related

"Rendered fewer hooks than expected", but there are no direct hooks

I get this error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
In itself, this is nothing new to me and I know how to fix this, but I can't figure it out in this case.
import reactStringReplace from 'react-string-replace';
import { Entity, Player } from './LogItem';
export default function PrepareText({
subjects,
text,
EntityPill,
PlayerPill,
}: {
subjects: { player: Player; entity: Entity };
text: string;
EntityPill: (text: string | null) => React.ReactNode;
PlayerPill: (text: string | null) => React.ReactNode;
}) {
return (
<>
{reactStringReplace(text, /(\${\w+})/g, (match, i) => {
const key = match
.replace('${', '')
.replace('}', '') as keyof typeof subjects;
return (
<span key={i}>
{key === 'player'
? PlayerPill(subjects[key])
: EntityPill(subjects[key])}
</span>
);
})}
</>
);
}
I think the problem is with conditionally rendering the two components PlayerPill and EntityPill because these use hooks inside. But usually, it's not e problem to conditionally render components.
Is it because I call them as functions? Is there e different way to pass props to a React.ReactNode?
If there is a better option to do this I would be very excited to implement it.
Edit
As requested here is the code of the Pills:
import useSteamUser from '../../../../hooks/useSteamUser';
import { CellProps } from '../LogItem';
export default function TargetCell({ value: text, restricted }: CellProps) {
return (
<span className="text-cyan-700 bg-cyan-700/5 px-2 py-0.5 rounded-full border border-cyan-700 text-sm">
{text}
</span>
);
}
and these pills are stored in an Object to be dynamically accessed like this:
NAME_CHANGED: {
Icon: (
<GiBodySwapping className="text-3xl fill-sand-500/60 group-hover:fill-sand-500 transition-colors" />
),
text: "You've changed your name!",
EntityCell: PlayerCell,
PlayerCell: PlayerCell,
},
Call of PrepareText. My real problem is that I need to pass text to the Pills but the Pills are used in PrepareText.
<PrepareText
subjects={{
player: data.player,
entity: data.entity,
}}
text={EVENTS[event].text}
EntityPill={(text) =>
EVENTS[event].EntityCell({ value: text || '', restricted })
}
PlayerPill={(text) =>
EVENTS[event].PlayerCell({ value: text || '', restricted })
}
/>
Sorry, I'm confused: why don't you simply do this:
return (
<span key={i}>
{key === 'player'
? <PlayerPill text={subjects[key]}/>
: <EntityPill text={subjects[key]}/>
}
</span>
);
Is it because I call them as functions?
Yes. You're not using them as components, you're using them as sub-functions of PrepareText. That means they use the component context of PrepareText, not their own context. So hooks save information to the underlying PrepareText instance, not their own, and so if the number of times you call them varies from render to render, it won't work correctly.
Instead, make them actual components and pass them information as props, not arguments; or make them render components using the arguments you pass them. (See also my answer to this related question.)
Here's an example of passing component functions (CompA/CompB, passed as Sub1/Sub2) to another component (Example) that uses them conditionally based on a 50/50 coin flip:
const { useState, useEffect } = React;
const flipCoin = () => Math.random() < 0.5;
const CompA = ({text}) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => c + 1);
}, 800);
}, []);
return <div>CompA, {text}, counter = {counter}</div>;
};
const CompB = ({text}) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => c + 1);
}, 800);
}, []);
return <div>CompB, {text}, counter = {counter}</div>;
};
const Example = ({Sub1, Sub2}) => {
const [counter, setCounter] = useState(0);
return <div>
<input type="button" value="Re-render" onClick={() => setCounter(c => c + 1)} />
{flipCoin() && <Sub1 text="a" />}
{flipCoin() && <Sub2 text="b" />}
</div>;
};
const App = () => {
return <Example Sub1={CompA} Sub2={CompB} />;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
The components use hooks so that you can see that it works just fine that Example doesn't always render them when it renders (because of the flipCoin calls).

ReactJS Custom Hook not rendering the expected output

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>

Force a single re-render with useSelector

This is a follow-up to Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function
I've declared a functional component Parameter that pulls in values from actions/reducers using the useSelector hook:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
}
);
};
const classes = useStyles();
return (
<div>
{drawerOpen ? (
Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterCurrent[key]}//This is where the change should be reflected in the radio button
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
) : (
<div />
)
}
</div >
)
};
export default Parameter;
What I need to have happen is for value={parameterCurrent[key]} to rerender on handleParameterChange (the handleChange does update the underlying dashboard data, but the radio button doesn't show as being selected until I close the main component and reopen it). I thought I had a solution where I forced a rerender, but because this is a smaller component that is part of a larger one, it was breaking the other parts of the component (i.e. it was re-rendering and preventing the other component from getting state/props from it's reducers). I've been on the internet searching for solutions for 2 days and haven't found anything that works yet. Any help is really apprecaited! TIA!
useSelector() uses strict === reference equality checks by default, not shallow equality.
To use shallow equal check, use this
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Read more
Ok, after a lot of iteration, I found a way to make it work (I'm sure this isn't the prettiest or most efficient, but it works, so I'm going with it). I've posted the code with changes below.
I added the updateState and forceUpdate lines when declaring the overall Parameter function:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
Then added the forceUpdate() line here:
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
forceUpdate() //added here
}
);
};
Then called forceUpdate in the return statement on the item I wanted to re-render:
<RadioGroup
aria-label="parameter"
name="parameter"
value={forceUpdate, parameterCurrent[key]}//added forceUpdate here
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
I've tested this, and it doesn't break any of the other code. Thanks!

React Hooks - How to use a make a state dependent on another state?

I recently started using hooks in react and I am often having this problem: I create a first big state which is used by all my component, but some of the smaller parts of my component divide this state and create their own state for simplicity.
For example
import React, { useState } from "react";
const initialFilters = {
name: "",
code: ""
};
function Filter({ value, setFilters }) {
const [tempValue, setTempValue] = useState(value);
return (
<input
value={tempValue}
onChange={e => setTempValue(e.target.value)}
onBlur={() => setFilters(tempValue)}
/>
);
}
function App() {
const [filters, setFilters] = useState(initialFilters);
const agents = [
{ name: "bob", code: "123" },
{ name: "burger", code: "3123" },
{ name: "sponge", code: "34" }
];
return (
<div>
<label>Name filter</label>
<Filter
value={filters.name}
setFilters={value =>
setFilters(filters => ({ ...filters, name: value }))
}
/>
<label>Code filter</label>
<Filter
value={filters.code}
setFilters={value =>
setFilters(filters => ({ ...filters, code: value }))
}
/>
<button onClick={() => setFilters(initialFilters)}>Reset filters</button>
<ul>
{agents
.filter(
agent =>
agent.name.includes(filters.name) &&
agent.code.includes(filters.code)
)
.map((agent, i) => (
<li key={i}>
name: {agent.name} - code: {agent.code}
</li>
))}
</ul>
</div>
);
}
export default App;
CodeSandox available here
In this example the filters work fine, but their value are not emptied when we use the button Reset.
The filters create their own states to dispatch new state only on blur, and still be controlled. I guess I could use ref here, but I use this example to showcase a simple case of state dependent on another state (and therefore on props).
How should I go about implementing this in an idiomatic React way?
You can use a useEffect hook. The 1st argument is a function and the 2nd argument is an array of dependencies. When a dependency changes value, the function is executed again.
import { useEffect } from 'react';
// ....code removed....
useEffect(() => {
setTempValue(value);
}, [value]);
// ....code removed....
Sandbox with changes: https://codesandbox.io/s/kind-bogdan-ljugv
As you can read in the documentation (https://reactjs.org/docs/hooks-state.html#declaring-a-state-variable), your state is created on the first render only and only then it is equal to your initial value.
You could write a custom hook useFilter and expose your filter-reseter:
const useFilter = (value, setFilters) => {
const [tempValue, setTempValue] = useState(value);
const resetFilter = () => setTempValue(value)
return {
resetFilter,
getInputProps: () => ({
onChange: e => setTempValue(e.target.value),
onBlur: () => setFilters(tempValue),
value: tempValue,
})
}
and instead of doing:
<Filter
value={filters.name}
setFilters={value =>
setFilters(filters => ({ ...filters, name: value }))
}
/>
do this:
const setFilters = value => setFilters(filters => ({ ...filters, name: value }))
const { resetTempFilter, getInputProps } = useFilter(value, setFilters)
...
<input {...getInputProps()} />
In this case you much easier to re-instantiate children with changing key prop(so named "reset state" technique based on how reconciliation works in React):
const [resetKey, setResetKey] = useState(0);
const doReset = setResetKey(key => key + 1);
<Filter
key={`name-filter-${resetKey}`}
...
/>
<Filter
key={`code-filter-${resetKey}`}
...
/>
<button onClick={doReset}>Reset!</button>
It's not only easier to achieve. It also will work the same for any stateful components you cannot modify(for any reason).

How to test if React state is reloaded from localStorage?

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).

Categories