So I have a Context of the following format:
class UserProvider extends Component {
constructor(props) {
super(props)
this.initialize = (details) => {
this.setState(state => {
//Setting each item individually here
//e.g state.a = details.a
})
}
this.editA = () => {
this.setState(state => {
//change A here
})
}
this.editB = () => {
this.setState(state => {
//Change B here
})
}
this.state = {
a: null,
b: null,
editA: this.editA,
editB: this.editB
}
}
render() {
return (
<User.Provider value={this.state}>
{this.props.children}
</User.Provider>
)
}
}
So for each state, I have a separate function to update it. If I want to update only a single state, what should I do?
Consider implementing a generic function so that you can control your key and the corresponding value.
i.e.
const changeField = (key, value) => this.setState({ [key]: value});
Function call
changeField('a','value_a')
changeField('b','value_b')
Related
I have the following code inside render and when I console log it, the correct values appear:
render() {
const { total } = this.state;
const filter = this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket))
console.log(filter)
console.log(this.state.filtered)
}
But when I do the same for a componentDidMount() hook it doesn't work.
export class Products extends React.Component {
constructor(props){
super(props)
this.state = {
filtered: []
};
}
componentDidMount() {
console.log(this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket)))
const filter = this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket))
this.setState({
filtered: filter
})
}
renderProductCards() {
return <ProductCard product={this.props?.catalog}/>
}
render() {
const { total } = this.state;
const filter = this.props.catalog.filter(word => word.markets.some(m => m === this.props.currentMarket))
console.log(filter)
console.log(this.state.filtered)
return (
<div>
<div className="ui divider"></div>
</div>
);
}
}
So for the first render hook I get the correct values of the filter but for componentDidMount() I get nothing as the value, I get the state value but it doesn't setState correctly and when I console log it it appears nothing.
When updating state using previous state or props, the callback argument should be used.
See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
It should be
componentDidMount() {
this.setState((state, props)=> ({
filtered: props.catalog.filter((word)=>(
word.markets.some(m => m == props.currentMarket)
))
}));
}
I assume that you didn't call super(props) in your constructor. Try this:
constructor(props){
super(props); // Add this line to constructor
this.state = {
//states
};
}
I'm building a search field that is fetching from a data base upon users input and I'm struggling a bit. At the moment, it is fetching data in every keystroke, which is not ideal. I have looked at different answers and it seems that the best option is to do this in componentDidUpdate() and get a ref of the input feel to compare this with the current value through a setTimeout().
I have tried this, but I'm still fetching during every keystroke, not sure why? See a sample of the component below:
class ItemsHolder extends Component {
componentDidMount() {
//ensures the page is reloaded at the top when routing
window.scrollTo(0, 0);
this.props.onFetchItems(this.props.search);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.search !== this.props.search) {
console.log(
this.props.search ===
this.props.searchRef.current.props.value.toUpperCase()
);
setTimeout(() => {
console.log(
this.props.search ===
this.props.searchRef.current.props.value.toUpperCase()
);
if (
this.props.search ===
this.props.searchRef.current.props.value.toUpperCase()
) {
this.props.onFetchItems(this.props.search, this.props.category);
}
}, 500);
}
}
I'm using Redux for state management. Here is the function that is called when fetching items:
export const fetchItemsFromServer = (search) => {
return (dispatch) => {
dispatch(fetchItemsStart());
const query =
search.length === 0 ? '' : `?orderBy="country"&equalTo="${search}"`;
axios
.get('/items.json' + query)
.then((res) => {
const fetchedItems = [];
for (let item in res.data) {
fetchedItems.push({
...res.data[item],
id: item,
});
}
dispatch(fetchItemsSuccess(fetchedItems));
})
.catch((error) => {
dispatch(fetchItemsFail(error));
});
};
};
This is how I'm setting the ref in the search component:
class Search extends Component {
constructor(props) {
super(props);
this.searchInput = React.createRef();
}
componentDidMount() {
this.props.onSetRef(this.searchInput);
}
render() {
return (
<Input
ref={this.searchInput}
toolbar
elementType={this.props.inputC.elementType}
elementConfig={this.props.inputC.elementConfig}
value={this.props.inputC.value}
changed={(event) => this.props.onChangedHandler(event)}
/>
);
}
}
Based on a tutorial I found this should work. For your reference, see the code from this tutorial. I don't see why wouldn't the above work. The only difference is that the tutorial uses hooks.
const Search = React.memo(props => {
const { onLoadIngredients } = props;
const [enteredFilter, setEnteredFilter] = useState('');
const inputRef = useRef();
useEffect(() => {
const timer = setTimeout(() => {
if (enteredFilter === inputRef.current.value) {
const query =
enteredFilter.length === 0
? ''
: `?orderBy="title"&equalTo="${enteredFilter}"`;
fetch(
'https://react-hooks-update.firebaseio.com/ingredients.json' + query
)
.then(response => response.json())
.then(responseData => {
const loadedIngredients = [];
for (const key in responseData) {
loadedIngredients.push({
id: key,
title: responseData[key].title,
amount: responseData[key].amount
});
}
onLoadIngredients(loadedIngredients);
});
}
}, 500);
return () => {
clearTimeout(timer);
};
}, [enteredFilter, onLoadIngredients, inputRef]);
Following recommendation to debounceInput:
import React, { Component } from 'react';
// import classes from './Search.css';
import Input from '../../UI/Input/Input';
// redux
import * as actions from '../../../store/actions/index';
import { connect } from 'react-redux';
class Search extends Component {
componentDidUpdate(prevProps, prevState) {
if (prevProps.search !== this.props.search) {
this.props.onFetchItems(this.props.search, this.props.category);
}
}
debounceInput = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
};
render() {
return (
<Input
toolbar
elementType={this.props.inputC.elementType}
elementConfig={this.props.inputC.elementConfig}
value={this.props.inputC.value}
changed={(event) =>
this.debounceInput(this.props.onChangedHandler(event), 500)
}
/>
);
}
}
const mapStateToProps = (state) => {
return {
inputC: state.filtersR.inputConfig,
search: state.filtersR.search,
};
};
const mapDispatchToProps = (dispatch) => {
return {
onChangedHandler: (event) => dispatch(actions.inputHandler(event)),
onFetchItems: (search, category) =>
dispatch(actions.fetchItemsFromServer(search, category)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Search);
Here is the final solution after help here:
import React, { Component } from 'react';
// import classes from './Search.css';
import Input from '../../UI/Input/Input';
// redux
import * as actions from '../../../store/actions/index';
import { connect } from 'react-redux';
const debounceInput = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
};
class Search extends Component {
componentDidUpdate(prevProps, _prevState) {
if (prevProps.search !== this.props.search) {
this.responseHandler();
}
}
responseHandler = debounceInput(() => {
this.props.onFetchItems(this.props.search, this.props.category);
}, 1000);
render() {
return (
<Input
toolbar
elementType={this.props.inputC.elementType}
elementConfig={this.props.inputC.elementConfig}
value={this.props.inputC.value}
changed={(event) => this.props.onChangedHandler(event)}
/>
);
}
}
const mapStateToProps = (state) => {
return {
inputC: state.filtersR.inputConfig,
search: state.filtersR.search,
};
};
const mapDispatchToProps = (dispatch) => {
return {
onChangedHandler: (event) => dispatch(actions.inputHandler(event)),
onFetchItems: (search, category) =>
dispatch(actions.fetchItemsFromServer(search, category)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Search);
You really just need to debounce your input's onChange handler, or better, the function that is actually doing the asynchronous work.
Very simple debouncing higher order function:
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
Example Use:
fetchData = debounce(() => fetch(.....).then(....), 500);
componentDidUpdate(.......) {
// input value different, call fetchData
}
<Input
toolbar
elementType={this.props.inputC.elementType}
elementConfig={this.props.inputC.elementConfig}
value={this.props.inputC.value}
changed={this.props.onChangedHandler}
/>
Demo Code
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(fn, delay, [...args]);
};
};
const fetch = (url, options) => {
console.log("Fetching", url);
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetch Resolved");
resolve(`response - ${Math.floor(Math.random() * 1000)}`);
}, 2000);
});
};
export default class App extends Component {
state = {
search: "",
response: ""
};
changeHandler = (e) => {
const { value } = e.target;
console.log("search", value);
this.setState({ search: value });
};
fetchData = debounce(() => {
const { search } = this.state;
const query = search.length ? `?orderBy="country"&equalTo="${search}"` : "";
fetch(
"https://react-hooks-update.firebaseio.com/ingredients.json" + query
).then((response) => this.setState({ response }));
}, 500);
componentDidUpdate(prevProps, prevState) {
if (prevState.search !== this.state.search) {
if (this.state.response) {
this.setState({ response: "" });
}
this.fetchData();
}
}
render() {
const { response, search } = this.state;
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<label>
Search
<input type="text" value={search} onChange={this.changeHandler} />
</label>
<div>Debounced Response: {response}</div>
</div>
);
}
}
I am building out a switch which reads it's checked value from the value I retrieve from the backend, and when a user toggles it, a PUT request is sent to backend to update the choice.
This is what I have so far, the toggle isn't displaying the response, what am I missing here
class Test extends Component {
constructor(props) {
super(props);
this.state = {
toggleValue: ''
}
}
componentDidMount() {
this.props.getToggleValue();
}
toggleValue = () => {
if(this.props.value){
return this.props.value.toggleValue
}
}
handleChange(field) {
return (event) => {
this.setState({
[field]: event.target.value,
});
}
}
render() {
return (
<>
<Switch value={this.state.toggleValue} checked={this.toggleValue()} onChange={this.handleChange('toggleValue')} />
</>
)
}
const mapStateToProps = (state, ownProps) => {
return {
...ownProps,
value: state.testReducer.value
};
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
getToggleValue
}, dispatch);
};
You have to check for event.target.checked and not event.target.value
handleChange(field) {
return (event) => {
this.setState({
[field]: event.target.checked,
});
}
}
I have the following TypeScript code. I simplified/remove as much as I could.
interface DataPullingPageState
{
loading: boolean;
displayedEntries: string[];
};
export class EntriesPageOne extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const entries = await api.loadAll();
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
export class EntriesPageTwo extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const param = "my param";
const entries = await api.loadByStringParam(param);
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
export class EntriesPageThree extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const param = 123;
const entries = await api.loadByNumberParam(param);
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
As you can see it's three different components that all display the same but have three different ways of loading it.
I'd like to know how I can make only one component out of those three. I've already heard about HoC but don't know if they suit my case.
Yes you can HoC let's simplify your code a bit:
HoC Method
class EntriesPage extends React.Component {
// you don't need state for loading
render() {
const { loading, entries } = this.props
}
}
EntriesPage.defaultProps = { loading: true, entries: [] }
const withEntries = (apiCall) => (Page) => {
return async (props) => {
const entries = await apiCall()
<Page {...props} loading={false} entries={entries} />
}
}
Now you can compose first page like this
// PageOne
export default withEntries(api.loadAll)(EntriesPage)
// PageTwo
export default withEntries(() => api.loadByStringParam('param'))(EntriesPage)
// PageThree
export default withEntries(() => api.loadByNumberParam(123))(EntriesPage)
This will create HoC which accepts dynamic fetching method and pass the result as prop to the final component. Hope this helps
Hoc method with param as prop
You can even expose params to the final component by changing it to something like this
const withEntries = (apiCall) => (Page) => {
return async (props) => {
const { fetchParam, ...rest } = props
const entries = await apiCall(fetchParam)
<Page {...rest} loading={false} entries={entries} />
}
}
// EntriesPageComposed.tsx
export default withEntries(api.loadByStringParam)(EntriesPage)
<EntriesPageComposed fetchParams={123} />
Loader component
Or you can even make it completely isolated without HoC and pass everything as prop and make "data loader" component, which is quite common pattern in React apps, which will act only as loader for preparing next props.
const ComposedComponent = async (props) => {
const { fetchMethod, fetchParam, ...rest } = props
const entries = await fetchMethod(fetchParam)
return (
<EntriesPage {...rest} loading={false} entries={entries} />
)
}
<ComposedComponent fetchMethod={api.loadByStringParam} fetchParam={'param'} />
In this way you have initial implementation isolated and you can add new fetch methods on the fly just by passing a prop.
I have this code:
export default class MainStudentPage extends React.Component {
constructor(props) {
super(props);
this.state = {user: {nickname: '', friends: {accepted: [], invites: [], all: []}}};
}
componentWillMount() {
const {uid} = firebase.auth().currentUser;
firebase.database().ref('Users').child(uid).on('value', (r, e) => {
if (e) {
console.log(e);
return null;
}
const user = r.val();
this.setState({user: user});
});
}
render() {
const {user} = this.state;
return (
<LevelSelectComponent user={user}/>
</div>
);
}
}
And this is the child:
export default class LevelSelectComponent extends React.Component {
returnSelect = (user) => {
const lvls = [{
db: 'Podstawówka',
text: 'PODSTAWOWA'
}, {
db: 'Gimnazjum',
text: 'GIMNAZJALNA'
}, {
db: 'Liceum',
text: 'ŚREDNIA'
}];
let options = [];
if (!user.level) {
options.push(<option selected={true} value={null}>WYBIERZ POZIOM</option>)
}
options = options.concat(lvls.map((lvl, i) => {
return (
<option key={i} value={lvl.db}>{`SZKOŁA ${lvl.text}`}</option>
)
}));
return (
<select defaultValue={user.level}>
{options.map(opt => opt)}
</select>
)
};
render() {
const {user} = this.props;
return (
this.returnSelect(user)
);
}
}
So what I want is to refresh the default selected value to match the value in the database. I am listening to the firebase realtime database for changes. Every time I refresh the page, the defaultValue changes, as expected, but this doesn't do it in real time. It even logs the new value, but it doesn't rerender it. What am I missing?
Ok. All I had to do was change defaultValue to value
componentWillMount() this should not be the method where you use AJAX requests
instead, user componentDidMount().
Further:
componentWillMount() will only be invoked once, before the first render() for your component, thus it will not trigger a re-render for it, you should subscribe to your firebase realtime events in componentDidMount().