React and redux not updating component until second re-render - reactjs

I have a react component written in typescript that has an input field and a button. The user can type an input and after pressing the button, a table will be populated with the relevant results. I use React.useEffect() to only run the search code if the searchTerm has changed. Running the code inside useEffect() will populate the table rows which then get stored for display in the able component.
export const SearchWindow: React.FC<Props> = props => {
const [searchInput, setSearchInput] = React.useState(''); // text value inside input box
const [searchTerm, setSearchTerm] = React.useState(''); // term to search for (set once search clicked)
const handleSearchInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchInput(event.target.value);
};
React.useEffect(() => {
if (searchTerm.length > 0) {
getSearchResults(props.config, searchTerm).then(async searchResults => {
const rows = searchResults.map(result => {
return {
cells: {
text: result.text,
},
};
});
store.dispatch(setSearchResults(rows));
});
}
}, [searchTerm]); // Only run search if searchTerm changes
// on 1st search, prints blank until another character is typed
console.log(store.getState().documentationIndex.searchResults);
return (
<form>
<input
type='search'
placeholder='Enter Search Term'
onChange={handleSearchInputChange}
value={searchInput}
/>
<<button onClick={() => setSearchTerm(searchInput)}>Search</button>
</form>
<Table
...
...
rows={store.getState().documentationIndex.searchResults}
/>
);
};
// -------------- store code ----------------------------
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
// Reducer
export interface DocumentationIndexState {
searchResults: DataTableRow<TableSchema>[];
}
const initialState: DocumentationIndexState = {
searchResults: [],
};
const store = createSlice({
name: 'documentationIndex',
initialState,
reducers: {
setSearchResults: (state, action: PayloadAction<DataTableRow<TableSchema>[]>) => {
state.searchResults = action.payload;
},
},
});
export default store.reducer;
export const {
setSearchResults,
} = store.actions;
The code behaves as expected except for on the first search. Take the following sequence:
User inputs 'hello' into search input box and clicks 'Search' button.
Search is run successfully but nothing is displayed in the Table component.
User types any random character following 'hello' which already exists in the input box.
Table component is updated with the search results for 'hello' successfully.
User deletes 'hello' from input box and types in 'foobar' then presses 'Search' button.
Search results for 'foobar' are displayed correctly
When I print
console.log(store.getState().documentationIndex.searchResults);
right before the return(.... that renders the component, the results are blank after the first search. When I type one more character, the results populate.
I'm at the end of my wits as to why this is happening so any help would be appreciated!

Never use store.getState in a React component. Use useSelector
const searchResults = useSelector(state => state.documentationIndex.searchResults)
Otherwise your component will not update when the state updates.

Related

Most idiomatic way to only run React hook if its dependency changed from the last value

I have a react app consisting of ParentComponent and HelpSearchWindow. On the page for ParentComponent, there is a button that lets you open up a window containing HelpSearchWindow. HelpSearchWindow has an input field and a search button. When an input is typed and search button is clicked, a search is run and results are displayed to the table on the window. The window can be closed. I have set up a react.useEffect() hook with the dependency [documentationIndexState.searchTerm] so that the search functionality is only run if the searchTerm changes.
However, the window was not behaving as I expected it to. Since useEffect() was being called every time the window was opened after it was closed, it would run the search again no matter if the searchTerm in the dependency array was the same. Because of this, I added another state prop (prevSearchTerm) from ParentComponent to store the last searched term. This way if the window is opened and closed multiple times without a new searchTerm being set, there is no repeat search run.
My question is, is there a more idiomatic/react-ish way to do this? Any other code formatting pointers are welcome as well
import {
setSearchTerm,
} from 'documentationIndex.store';
interface Props {
searchInput: string;
setSearchInput: (searchInput: string) => void;
prevSearchTerm: string;
setPrevSearchTerm: (searchInput: string) => void;
}
export const HelpSearchWindow: React.FC<Props> = props => {
const documentationIndexState = useSelector((store: StoreState) => store.documentationIndex);
const dispatch = useDispatch();
// Only run search if searchTerm changes
React.useEffect(() => {
async function asyncWrapper() {
if (!documentationIndexState.indexExists) {
// do some await stuff (need asyncWrapper because of this)
}
if (props.prevSearchTerm !== documentationIndexState.searchTerm) {
// searching for a term different than the previous searchTerm so run search
// store most recently used searchTerm as the prevSearchTerm
props.setPrevSearchTerm(props.searchInput);
}
}
asyncWrapper();
}, [documentationIndexState.searchTerm]);
return (
<input
value={props.searchInput}
onChange={e => props.setSearchInput(e.target.value)}
/>
<button
onClick={e => {
e.preventDefault();
dispatch(setSearchTerm(props.searchInput));
}}
>
Search
</button>
<SearchTable
rows={documentationIndexState.searchResults}
/>
);
};
//--------- Parent Component----------------------------------------
const ParentComponent = React.memo<{}>(({}) => {
const [searchInput, setSearchInput] = React.useState(''); // value inside input box
const [prevSearchTerm, setPrevSearchTerm] = React.useState(''); // tracks last searched thing
return(
<HelpSearchWindow
searchInput={searchInput}
setSearchInput={setSearchInput}
prevSearchTerm={prevSearchTerm}
setPrevSearchTerm={setPrevSearchTerm}
/>
);
});
From the given context, the use of useEffevt hook is redundant. You should simply use a click handler function and attach with the button.
The click handler will store the search term locally in the component and also check if the new input value is different. If it is itll update state and make the api call.

React Redux how to retrieve object from state array based on button click

I'm working on a project where a user can input multiple exercise names/weights. These go into a state array as objects. For example:
[
{
name: bench
weight: 100
},
{
name: squat:
weight: 200
}
]
Each of these objects becomes a button that displays the name and weight. What I want to happen is allow the user to click the button and navigate to another page where that specific name and weight are displayed, but I can't figure out how to do that. Currently, I am getting the whole state array instead of just the specific one clicked.
Here is my button component:
const MovementButtons = (props) => {
const navigate = useNavigate();
const {
move
} = props;
const mapMoves = move.map((movement) => {
return (
<Button
key={movement.name}
onClick={() => navigate(`/movement/${movement.name}/${movement.weight}`)}
>
{movement.name} - {movement.weight}
</Button>
)
})
return <div>{mapMoves}</div>
};
const mapStateToProps = state => {
return {
move: state.moveReducer
}
};
export default connect(mapStateToProps)(MovementButtons);
Here is the page where the button redirects to and where I want to try and get the name of the button that was clicked:
const PercentChart = (props) => {
const {
move
} = props;
return (
<div>
{move.name}
{move.weight}
</div>
);
};
const mapStateToProps = (state) => {
return {
move: state.moveReducer,
}
};
export default connect(mapStateToProps)(PercentChart);
So right now if I click on the button labeled "bench" for example, I get redirected to the PercentChart page, where instead of getting just the "bench" object I get the whole state array. Can anyone point me in the right direction on how I can get the specific object based on the button that was clicked? If I am missing code that needs to be shown let me know and I'll update.

Why is my data that is coming from apollo server not showing up when I refresh the page?

I am building a simple application using React, Apollo and React Router. This application allows you to create recipes, as well as edit and delete them (your standard CRUD website).
I thought about how I would present my problem, and I figured the best way was visually.
Here is the home page (localhost:3000):
When you click on the title of a recipe, this is what you see (localhost:3000/recipe/15):
If you click the 'create recipe' button on the home page, this is what you see (localhost:3000/create-recipe):
If you click on the delete button on a recipe on the home page, this is what you see (localhost:3000):
If you click on the edit button on a recipe on the home page, this is what you see (localhost:3000/recipe/15/update):
This update form is where the problem begins. As you can see, the form has been filled with the old values of the recipe. Everything is going to plan. But, when I refresh the page, this is what you see:
It's all blank. I am 67% sure this is something to do with the way React renders components or the way I am querying my apollo server. I don't fully understand the process React goes through to render a component.
Here is the code for the UpdateRecipe page (what you've probably been waiting for):
import React, { useState } from "react";
import { Button } from "#chakra-ui/react";
import {
useUpdateRecipeMutation,
useRecipeQuery,
useIngredientsQuery,
useStepsQuery,
} from "../../types/graphql";
import { useNavigate, useParams } from "react-router-dom";
import { SimpleFormControl } from "../../shared/SimpleFormControl";
import { MultiFormControl } from "../../shared/MultiFormControl";
interface UpdateRecipeProps {}
export const UpdateRecipe: React.FC<UpdateRecipeProps> = ({}) => {
let { id: recipeId } = useParams() as { id: string };
const intRecipeId = parseInt(recipeId);
const { data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const { data: ingredientsData } = useIngredientsQuery({
variables: { recipeId: intRecipeId },
});
const { data: stepsData } = useStepsQuery({
variables: { recipeId: intRecipeId },
});
const originalTitle = recipeData?.recipe.recipe?.title || "";
const originalDescription = recipeData?.recipe.recipe?.description || "";
const originalIngredients =
ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text) || [];
const originalSteps = stepsData?.steps?.steps?.map((stp) => stp.text) || [];
const [updateRecipe] = useUpdateRecipeMutation();
const navigate = useNavigate();
const [formValues, setFormValues] = useState({
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<SimpleFormControl
label="Title"
name="title"
type="text"
placeholder="Triple Chocolate Cake"
value={formValues.title}
onChange={(e) => {
setFormValues({ ...formValues, title: e.target.value });
}}
/>
<SimpleFormControl
label="Description"
name="description"
type="text"
placeholder="A delicious combination of cake and chocolate that's bound to mesmerize your tastebuds!"
value={formValues.description}
onChange={(e) => {
setFormValues({ ...formValues, description: e.target.value });
}}
/>
<MultiFormControl
label="Ingredients"
name="ingredients"
type="text"
placeholder="Eggs"
values={formValues.ingredients}
onAdd={(newValue) => {
setFormValues({
...formValues,
ingredients: [...formValues.ingredients, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
ingredients: formValues.ingredients.filter(
(__, idx) => idx !== index
),
});
}}
/>
<MultiFormControl
ordered
label="Steps"
name="steps"
type="text"
placeholder="Pour batter into cake tray"
color="orange.100"
values={formValues.steps}
onAdd={(newValue) => {
setFormValues({
...formValues,
steps: [...formValues.steps, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
steps: formValues.steps.filter((__, idx) => idx !== index),
});
}}
/>
<Button type="submit">Update Recipe</Button>
</form>
);
};
I'll try to explain it as best as I can.
First I get the id parameter from the url. With this id, I grab the corresponding recipe, its ingredients and its steps.
Next I put the title of the recipe, the description of the recipe, the ingredients of the recipe and the steps into four variables: originalTitle, originalDescription, originalIngredients and originalSteps, respectively.
Next I set up some state with useState(), called formValues. It looks like this:
{
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
}
Finally, I return a form which contains 4 component:
The first component is a SimpleFormControl and it is for the title. Notice how I set the value prop of this component to formValues.title.
The second component is also a SimpleFormControl and it is for the description, which has a value prop set to formValues.description.
The third component is a MultiFormControl and it's for the ingredients. This component has its value props set to formValues.ingredients.
The fourth component is also aMultiFormControl and it's for the steps. This component has its value props set to formValues.steps.
Let me know if you need to see the code for these two components.
Note:
When I come to the UpdateRecipe page via the home page, it works perfectly. As soon as I refresh the UpdateRecipe page, the originalTitle, originalDescripion, originalIngredients and originalSteps are either empty strings or empty arrays. This is due to the || operator attached to each variable.
Thanks in advance for any feedback and help.
Let me know if you need anything.
The problem is that you are using one hook useRecipeQuery that will return data at some point in the future and you have a second hook useState for your form that relies on this data. This means that when React will render this component the useRecipeQuery will return no data (since it's still fetching) so the useState hook used for your form is initialized with empty data. Once useRecipeQuery is done fetching it will reevaluate this code, but that doesn't have any effect on the useState hook for your form, since it's already initialized and has internally cached its state. The reason why it's working for you in one scenario, but not in the other, is that in one scenario your useRecipeQuery immediately returns the data available from cache, whereas in the other it needs to do the actual fetch to get it.
What is the solution?
Assume you don't have the data available for your form to properly render when you first load this component. So initialize your form with some acceptable empty state.
Use useEffect to wire your hooks, so that when useRecipeQuery finishes loading its data, it'll update your form state accordingly.
const { loading, data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const [formValues, setFormValues] = useState({
title: "",
description: "",
ingredients: [],
steps: [],
});
useEffect(() => {
if (!loading && recipeData ) {
setFormValues({
title: recipeData?.recipe.recipe?.title,
description: recipeData?.recipe.recipe?.description,
ingredients: ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text),
steps: stepsData?.steps?.steps?.map((stp) => stp.text),
});
}
}, [loading, recipeData ]);

How to update same redux state on two different components simultaneously?

I have a React Native scenario in which I have two components, one is a textInput and the other is a SVG drawing. By filling out the textInput and pressing a button, the text is inserted as a data instance to an array stored in a redux state using a dispatch action (let's call it textListState).
//TEXT INPUT COMPONENT
//this code is just for presentation. it will cause errors obviously
const dispatch = useDispatch();
const submitHandler = (enteredText) => {
dispatch(submitData(enteredText))
};
return(
<TextInput ...... />
<TouchableOpacity onPress={() => submitHandler(enteredText) } />
)
Now, the SVG component updates all the features' fill color stored in the textListState using a function (e.g. setFillColor). This is done inside a useEffect hook with a dependency set to a prop (e.g. propA).
Now the problem is that I need to add textListState as a dependency to the useEffect because I want the newly entered text from the textInput to be included in the SVG component. But by doing so, I am creating an infinite loop, because the setFillColor also updates the textListState.
//SVG COMPONENT
const [textList, setTextList] = useState([]);
const textListState = useSelector(state => state.textListState);
const dispatch = useDispatch();
const setFillColor= useCallback((id, fill) => {
dispatch(updateFillColor(id, fill))
}, [dispatch]);
useEffect(() => {
//INFINITE LOOP BECAUSE textListState KEEPS UPDATING WITH setFillColor
const curTextList = [];
for (const [i, v] of textListState.entries()) {
setFillColor(5, "white")
curTextList.push(
<someSVGComponent />
)
}
setTextList(curTextList);
}, [props.propA, textListState])
return(
<G>
{textList.map(x => x)}
</G>
)
How can I achieve to be able to add the newly inserted text to the SVG component without creating an infinite loop?
EDIT:
The redux action
export const UPDATE_TEXT = "UPDATE_TEXT";
export const SUBMIT_DATA = "SUBMIT_DATA";
export const updateFillColor = (id, fill) => {
return { type: UPDATE_TEXT, id: id, fill: fill }
};
export const submitData= (text) => {
return { type: SUBMIT_DATA, text: text }
};
The redux reducer
import { TEXT } from "../../data/texts";
import { UPDATE_TEXT } from "../actions/userActions";
import { INSERT_TEXT } from "../actions/userActions";
// Initial state when app launches
const initState = {
textListState: TEXT
};
const textReducer = (state = initState, action) => {
switch (action.type) {
case INSERT_TEXT :
return {
...state,
textListState: [
...state.textListState,
action.text
]
}
case UPDATE_TEXT :
const curText = state.textListState.filter(text => text.id === action.id)[0];
return {
...state,
textListState: [
...state.textListState.slice(0, curIndex), //everything before current text
curText.fill = action.fill,
...state.textListState.slice(curIndex + 1), //everything after current text
]
}
default:
return state;
}
};
export default textReducer;
if you want to trigger useEffect only when new text is added then use textListState.length instead of textListState in dependency list.
If you want to do it both on update and insert (i.e. you want to fire even when text is updated in input) then use a boolean flag textUpdated in textListState and keep toggling it whenever there is a update or insert and use textListState.textUpdated in dependency list.

Custom Autocomplete component not showing output when searching for the first time

I have created my custom Autocomplete (Autosuggestions) component. Everything works fine when I pass a hardcoded array of string to autocomplete component, but when I try to pass data from API as a prop, nothing is showing for the first time I search. Results are showing each time exactly after the first time
I have tried different options but seems like when a user is searching for the first time data is not there and autocomplete is rendered with an empty array. I have tested same API endpoint and it's returning data as it should every time you search.
Home component which holds Autocomplete
const filteredUsers = this.props.searchUsers.map((item) => item.firstName).filter((item) => item !== null);
const autocomplete = (
<AutoComplete
items={filteredUsers}
placeholder="Search..."
label="Search"
onTextChanged={this.searchUsers}
fieldName="Search"
formName="autocomplete"
/>
);
AutoComplete component which filters inserted data and shows a list of suggestions, the problem is maybe inside of onTextChange:
export class AutoComplete extends Component {
constructor(props) {
super(props);
this.state = {
suggestions: [],
text: '',
};
}
// Matching and filtering suggestions fetched from the backend and text that user has entered
onTextChanged = (e) => {
const value = e.target.value;
let suggestions = [];
if (value.length > 0) {
this.props.onTextChanged(value);
const regex = new RegExp(`^${value}`, 'i');
suggestions = this.props.items.sort().filter((v) => regex.test(v));
}
this.setState({ suggestions, text: value });
};
// Update state each time user press suggestion
suggestionSelected = (value) => {
this.setState(() => ({
text: value,
suggestions: []
}));
};
// User pressed the enter key
onPressEnter = (e) => {
if (e.keyCode === 13) {
this.props.onPressEnter(this.state.text);
}
};
render() {
const { text } = this.state;
return (
<div style={styles.autocompleteContainerStyles}>
<Field
label={this.props.placeholder}
onKeyDown={this.onPressEnter}
onFocus={this.props.onFocus}
name={this.props.fieldName}
formValue={text}
onChange={this.onTextChanged}
component={RenderAutocompleteField}
type="text"
/>
<Suggestions
suggestions={this.state.suggestions}
suggestionSelected={this.suggestionSelected}
theme="default"
/>
</div>
);
}
}
const styles = {
autocompleteContainerStyles: {
position: 'relative',
display: 'inline',
width: '100%'
}
};
AutoComplete.propTypes = {
items: PropTypes.array.isRequired,
placeholder: PropTypes.string.isRequired,
onTextChanged: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onPressEnter: PropTypes.func.isRequired,
onFocus: PropTypes.func
};
export default reduxForm({
form: 'Autocomplete'
})(AutoComplete);
Expected results: Every time user use textinput to search, he should get results of suggestions
Actual results: First-time user use textinput to search, he doesn't get data. Only after first-time data is there
It works when it is hardcoded but not when using your API because your filtering happens in onTextChanged. When it is hardcoded your AutoComplete has a value to work with the first time onTextChanged (this.props.items.sort().filter(...) is called but with the API your items prop will be empty until you API returns - after this function is done.
In order to handle results from your API you will need do the filtering when the props change. The react docs actually cover a very similar case here (see the second example as the first is showing how using getDerivedStateFromProps is unnecessarily complicated), the important part being they use a PureComponent to avoid unnecessary re-renders and then do the filtering in the render, e.g. in your case:
render() {
// Derive your filtered suggestions from your props in render - this way when your API updates your items prop, it will re-render with the new data
const { text } = this.state;
const regex = new RegExp(`^${text}`, 'i');
suggestions = this.props.items.sort().filter((v) => regex.test(v));
...
<Suggestions
suggestions={suggestions}
...
/>
...
}

Resources