Question about useContext and re-rendering - reactjs

Considering a situation where I have an App component, and Contexts components, called 'AuthContext' and 'usersAndProductsContext'.
In the context components I have few states, which are initialized in App component and from there will be given to other components to share the states like so:
return (
<div className="App">
<usersAndProductsContext.Provider
value={{
usersVal: usersVal,
productsVal: productsVal,
toggle_pressed_login_flag: toggle_pressed_login_flag,
handleAddUser: handleAddUser,
}}
>
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
edit_currentLoggedUserId: edit_currentLoggedUserId,
currentLoggedUserId: currentLoggedUserId,
isLoggedInEditVal: isLoggedInEditVal,
}}
>
<Routes>
<Route exact path="/" element={<Login />} />
<Route exact path="farmers" element={<Farmers />} />
<Route exact path="customer" element={<Customer />} />
<Route exact path="register" element={<Register />} />
<Route exact path="farmershop/:id" element={<Farmer />} />
<Route exact path="mystore/" element={<MyStore />} />
</Routes>
</AuthContext.Provider>
</usersAndProductsContext.Provider>
</div>
);
when I pass to other components, as I did using 'Provider', it means I'm sharing with all other components these states which are given within the 'value' prop.
Does that mean that every change of the passed-on-using-provider states, in other component, will mean that the information which was passed on in all components has now changed?
Or did it become some sort of local copy?
To emphasize what I mean:
function MyStore() {
let authCtx = useContext(AuthContext);
let usersAndItemsCtx = useContext(usersAndProductsContext);
...
if I change usersAndItemsCtx from MyStore, will it mean that all other component's state that I have changed?
Hope it made sense?
Regards!

It depends on what you mean by change. If you mean mutating the state then no, but if you mean update the state then yes.
If you do authCtx.someProperty = false;, then do not expect the other components to rerender. The actual way to update the context would by passing a setter function down the context and using it.
Looking at this, there seems to be a handleAddUser, if this is called and it updates the context only then a change will reflected in other components.
value={{
usersVal: usersVal,
productsVal: productsVal,
toggle_pressed_login_flag: toggle_pressed_login_flag,
handleAddUser: handleAddUser,
}}
Here is a sandbox where I have mutated the context. Note, how the setInterval logs the mutated value but there is no update to the UI because the component never rerendered. These kind of bugs are common when mutating state (or context by extension).
The relevant code:
const TextContext = createContext();
function App() {
const [text, setText] = useState({
val: "string",
count: 0
});
return (
<>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<TextContext.Provider value={[text, setText]}>
<Wrapper />
</TextContext.Provider>
</div>
</>
);
}
function Wrapper(props) {
return (
<div style={{ padding: "20px" }}>
<TextControlWithHook />
<br />
<Random />
</div>
);
}
function Random() {
const [text, setText] = useContext(TextContext);
const mutateContext = () => {
text.val = "1231232";
};
return <button onClick={mutateContext}>MUTATE</button>;
}
function TextControlWithHook(props) {
const [text, setText] = useContext(TextContext);
useEffect(() => {
console.log("outside", { text });
const id = setInterval(() => {
console.log(text);
}, 5000);
return () => clearInterval(id);
}, [text]);
return (
<input
className="form-control"
value={text.val}
onChange={(e) => {
const val = e.target.value;
setText((text) => ({ ...text, val }));
}}
/>
);
}

Related

React Router Link component not rendering when redirect

I'm using the material-ui library's component Autocomplete with React flux to create a Search Bar that finds users.
I want every option to be a link to a page with more information of that user. Currently, it only works the first time I click on an option. Every time after that it changes the URL but the page is not re-rendered.
This is the Autocomplete code:
export function SearchBar() {
const [url, setUrl] = useState("/perfil/");
const { store, actions } = useContext(Context);
useEffect(() => {
actions.search();
}, [url]);
const artistas = store.artistas.slice();
return (
<Autocomplete
id="autocomplete"
freeSolo
disableClearable
options={artistas}
getOptionLabel={(option) => option.nombre + " " + option.apellido}
renderOption={(option) => (
<Link
className="text-black text-decoration-none"
onClick={() => setUrl(url + option.id)}
to={`/perfil/${option.id}`}
>
{option.nombre} {option.apellido}
</Link>
)}
renderInput={(params) => (
<TextField
{...params}
size="small"
placeholder="Search for your artist"
margin="normal"
variant="outlined"
InputProps={{ ...params.InputProps, type: "search" }}
/>
)}
/>
);
}
This is the page where it is redirected:
export const Perfil = () => {
const { user_id } = useParams();
return (
<>
<About user_id={user_id} />
<Galeria user_id={user_id} />
</>
);
};
The flux action is this:
search: async () => {
try {
const response = await fetch(
process.env.BACKEND_URL + "/api/artistas"
);
const data = await response.json();
setStore({
artistas: data,
});
} catch (error) {
console.log("Error loading message from /api/artistas", error);
}
}
At last, this is the layout page:
return (
<div>
<BrowserRouter basename={basename}>
<ScrollToTop>
<Navbar />
<Routes>
<Route element={<Inicio />} path="/" />
<Route element={<Login />} path="/login" />
<Route element={<Registro />} path="/registro" />
<Route element={<Perfil />} path="/perfil/:user_id" />
<Route element={<Producto />} path="/producto/:theid" />
<Route
element={<ConfiguracionUsuario />}
path="/configuracion/:theid"
/>
<Route element={<h1> Not found! </h1>} />
</Routes>{" "}
<Footer />
</ScrollToTop>{" "}
</BrowserRouter>{" "}
</div>
);
I fixed it. The problem was in the Galeria component. It took the id by props:
useEffect(() => {
actions.producto_galeria(props.user_id);
}, []);
But we should have used a stored variable with the ID that changes on every click with a flux action, instead of props. So now, I get the ID as follows:
flux new action:
get_artista_id: (artista_id) => {
setStore({ artista_id: artista_id });
}
Link new onClick:
<Link
className="text-black text-decoration-none"
onClick={() => {
actions.get_artista_id(option.id);
}}
to={`/perfil/${option.id}`}
>
{option.nombre} {option.apellido}
</Link>
useEffect on Galeria to detect the change on the ID:
useEffect(() => {
actions.producto_galeria(store.artista_id);
}, [store.artista_id]);
Link tags work first move to url. After that onClick logic will start after moved link.
So if you want to use Link tag with onClick then i recommend two ways.
react-router-dom v5 or lower : just don't use 'to' property and just use onClick and in the onClick property you can use useHistory hook inside onClick.
You can use under 2. Option here too.
react-router-dom v6 or lower : use 'a' tag with onClick property you can use useNavigate hook inside onClick.

history.push is doing an extra redirect, and ignoring the search parameters, if the user is already on the redirect page

I have a simple Navbar with a Form within it:
const NavBar = () => {
let history = useHistory()
...
...
return (
...
<Form inline onSubmit={handleSubmit}>
<InputGroup style={{width: "90%"}}>
<Form.Control id="navbar-search" placeholder="Pesquise" size="sm"/>
<Form.Control as="select" size="sm">
<option>Ações</option>
<option>Declarações</option>
</Form.Control>
<InputGroup.Append size="sm">
<Button size="sm" type="submit" variant="outline-secondary">
<SearchIcon fontSize="small" />
</Button>
</InputGroup.Append>
</InputGroup>
</Form>
...
)
handleSubit is suppose to redirect (using history.push) to a path that I'm going to use the input value as a search parameter. I'm using react-router-dom.
const handleSubmit = (e) => {
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes"
history.push({
pathname: `/${baseEndpoint}/query`,
search: `?description=${e.target[0].value}`,
})
}
Everything looks good except if the user is in the page the handleSubmit is going to redirect, ie, at /${baseEndpoint}/query.
If the user is in this page, history goes to /${baseEndpoint}/query?description=${e.target[0].value} and re-renders automatically to /${baseEndpoint}/query?.
I've also tried using history.replace, but it didn't work.
const handleSubmit = (e) => {
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes"
let url = `/${baseEndpoint}/query`
if(history.location.pathname === url) {
history.replace({
pathname: url,
search: `?description=${e.target[0].value}`,
})
return
}
history.push({
pathname: url,
search: `?description=${e.target[0].value}`,
})
}
What is causing this behaviour? What am I doing wrong?
Thanks a lot!
(EDIT) My Switch and Routes:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
class Wrapper extends Component {
render() {
return (
<>
<Container className="main">
<MetaData />
<Navbar />
<this.props.component {...this.props} />
<br />
<br />
<Footer />
</Container>
</>
)
}
}
export default function App() {
return (
<Router>
<Switch>
<Route exact path="/"
render={props => (
<Wrapper {...props} component={Home} />
)}
/>
<Route
exact path="/actions"
key="action-home"
render={props => (
<Wrapper
{...props}
image={bozoAction}
baseEndpoint="actions"
component={EntityHome}
/>
)}
/>
<Route
path="/actions/query"
key="action-query"
render={props => (
<Wrapper
{...props}
image={bozoAction}
baseEndpoint="actions"
component={EntityQuery}
/>
)}
/>
<Route
path="/actions/:id(\d+)"
key="action-entity"
render={props => (
<Wrapper
{...props}
image={bozoAction}
baseEndpoint="actions"
component={Entity}
/>
)}
/>
<Route
exact path="/quotes"
key="quote-home"
render={props => (
<Wrapper
{...props}
image={bozoQuote}
baseEndpoint="quotes"
component={EntityHome}
/>
)}
/>
<Route
path="/quotes/query"
key="quote-query"
render={props => (
<Wrapper
{...props}
image={bozoQuote}
baseEndpoint="quotes"
component={EntityQuery}
/>
)}
/>
<Route
path="/quotes/:id(\d+)"
key="quote-entity"
render={props => (
<Wrapper
{...props}
image={bozoQuote}
baseEndpoint="quotes"
component={Entity}
/>
)}
/>
...
</Switch>
</Router>
)
}
Sandbox: https://codesandbox.io/s/0y8cm
Form some reason the sandbox is not redirecting and ignoring the search parameters, but we can evaluate the error doing the following: use the navbar to search one of the entities. Check the form (clicking in "Mais filtros") and see the provided query param at the input. Do another search in the navbar to the same entity. Check the form and see no initial values.
What's causing the behavior is the form submission event, it refreshes the page resulting in the loss of the query params, the solution to this is e.preventDefault() when you submit the form :
const handleSubmit = (e) => {
e.preventDefault(); // prevent the page refresh
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes";
history.push({
pathname: `/${baseEndpoint}/query`,
search: `?description=${e.target[0].value}`
});
};
You can add history.go() if you absolutely have to refresh the page, but i wouldn't recommend that, the goal is to let the child components know that the search has changed,
The way your EntityQuery and QueryForm is set up makes it impossible for the child components to know if there is a change, so you need to fix that,
You shouldn't set the initial state value from the props in the constructor as it's anti-pattern , instead, have the initial values empty and use life cycle methods to update them ( componentDidMount and componentDidUpdate )
EntityQuery
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: true,
hasMore: false,
searchParams: {
page: 1,
tags: "",
description: ""
},
entities: []
}
}
updateStateValues = () => {
const _initialTag = this.props.location.search.includes("tags")
? this.props.location.search.split("?tags=")[1]
: "";
const _initialText = this.props.location.search.includes("description")
? this.props.location.search.split("?description=")[1]
: "";
this.setState({
searchParams: {
page: 1,
tags: _initialTag,
description: _initialText
}
})
}
componentDidMount() {
this.updateStateValues()
}
componentDidUpdate(prevProps) {
// add checks for other potential props that you need to update if they change
if (prevProps.location.search.split("?description=")[1] !== this.props.location.search.split("?description=")[1])
this.updateStateValues()
}
And pass the values from the state to the child :
<QueryForm
baseEndpoint={this.props.baseEndpoint}
tagsInitialValues={this.state.searchParams.tags}
textInitialValue={this.state.searchParams.description}
setSearchParams={this.setSearchParams}
/>
Updated CodePen
EDIT:
Fllowing up the OP's comment and the updated SandBox, there's another issue regarding Formik where the initialValues do not update when the props change, see : https://github.com/formium/formik/issues/811
The suggested solution of adding enableReinitialize didn't work, so, to force the component to update, you can use a key that changes when the url changes, in this case, use this.props.textInitialValue :
QueryForm :
<Formik
key={this.props.textInitialValue}
...
Try adding e.prevenDefault() at the start of the handleSubmit function.
Maybe <form> submission is causing a page reload and hence, removing the search parameters.
Try to prevent the default behavior of the form on submission by changing your handleSubmit() handler to:
const handleSubmit = (e) => {
e.preventDefault()
let baseEndpoint = e.target[1].value === "Ações" ? "actions" : "quotes"
history.push({
pathname: `/${baseEndpoint}/query`,
search: `?description=${e.target[0].value}`,
})
}
Let me if it works *

React Hooks: Component is not rendering in Nested Routing

I am using "react-scripts": "4.0.2" and all my components are React Hooks. My logic involves nested routing but the end result is not rendered.
App.js:
<BrowserRouter>
<div className="App">
<TopNav />
<Home />
</div>
</BrowserRouter>
Home.js:
<Switch>
<Route path="/" component={Questions} />
</Switch>
Questions.js
const displayQuestion = (qid) => {
props.history.push({ pathname: "/question/" + qid });
};
//questions is an array of objects
const questionsBlocks = questions.map((quest, i) => {
return (
<QBlock
key={i + 1}
qno={i + 1}
displayQuestion={displayQuestion.bind(this, quest.qid)}
/>
);
});
return (
<div>
<h1>Questions</h1>
{questionsBlocks}
<Switch>
<Route
path="/question/:id"
render={(props) => <Question {...props} questions={questions} />}
/>
</Switch>
</div>
);
QBlock will only render buttons that will call displayQuestion on being clicked
QBlock.js:
return (
<div className="block" onClick={props.displayQuestion}>
<h1>{props.qno}</h1>
</div>
);
Question.js:
const [question, setQuestion] = useState();
const loadQuestion = () => {
console.log(props);
if (props.match.params.id) {
console.log("load called");
const qid = props.match.params.id;
const index = props.questions.findIndex((quest) => quest.qid == qid);
setQuestion(props.questions[index]);
}
};
// componentDidMount with react hooks
useEffect(() => {
console.log("Mounted");
loadQuestion();
}, []);
// componentDidUpdate with react hooks
useEffect(() => {
console.log("Updated");
loadQuestion();
}, [props.match.params.id]); //Even tried with only props
return (
<div className="Quest">
<div className="question">{question.question}</div>
<div className="options">{question.answerChoices}</div>
</div>
);
Neither of the useEffect of Question.js is not executing still I am getting the following error.
Basically, question needs to be initialized
const [question, setQuestion] = useState(null);
And another thing you need to do is to check the value of question before using it
return (
<div className="Quest">
{question && question.question && <div className="question">{question.question}</div>}
{question && question.answerChoices && <div className="options">{question.answerChoices}</div>}
</div>
);
As Vince said.. I had defined useState like const [question, setQuestion] = useState(); instead of const [question, setQuestion] = useState({});

Can you partially apply a component to some props in React?

I have 2 different components AnimeList and MangaList that share similar logic, so I want to share code between them. Here is how I'm currently doing it:
const AnimeList = (props) => <AnimangaList isAnime={true} {...props} />;
const MangaList = (props) => <AnimangaList isAnime={false} {...props} />;
return (
<Tab.Navigator>
<Tab.Screen name="Anime" component={AnimeList} />
<Tab.Screen name="Manga" component={MangaList} />
</Tab.Navigator>
);
Is there a shorter or more convenient way to do this in React / React Native? ie. Something similar to function.bind()? This feels pretty hefty.
I usually go with static array when I need to have different values for few properties on large amount of components.
const pages = [
{
isAnime: true,
name: 'Anime',
},
{
isAnime: false,
name: 'Manga',
},
];
// mocks for some components
const MangaList = ({ isAnime }) => <div>{isAnime}</div>;
const Screen = ({ name, component: Component }) => (
<div>
{name}
<Component />
</div>
);
And then inside Tab.Navigator:
{pages.map((p, i) => (
<Screen key={i} name={p.name} component={(props) => <MangaList isAnime={p.isAnime} {...props} />} />
))}

Can't display the output of a search on another component

Im having problems at the result component i can't render the filteredData, at result this.props.dataToRender ain't working thats the problem.
I have all the logic already, but when trying to display the result of the search on another component i'm getting no information.
If i execute the code on the app component i get the result of the search but if i try to pass it as props i don't get a result at the other side.
state = { data: [], filteredData: [], value: "" };
async componentDidMount() {
axios
.get("https://tenjoucesar.github.io/data.json")
.then(response => response.data)
.then(data => {
this.setState({ data });
console.log(this.state.data);
});
}
handleSubmit(e) {
const { value, data } = this.state;
e.preventDefault();
let email = value;
const filtered = data.filter(e => e.email === email);
this.setState({ filteredData: filtered });
}
handleChange(e) {
this.setState({ value: e.target.value });
}
InfomData = person => {
return (
<div className="result" key={person.email}>
<h3 >
{person.name}, {person.age}
</h3>
<p className="paragraph--result">{person.notes}</p>
</div>
);
};
render() {
const { filteredData } = this.state;
const dataToRender = filteredData.map(e => this.InfomData(e));
return (
<React.Fragment>
<Nav />
<Switch>
<>
<Route exact path="/" component={Header} />
<Search
component={Search}
handleSubmit={this.handleSubmit.bind(this)}
handleChange={this.handleChange.bind(this)}
value={this.props.value}
type={this.props.email}
/>
<Route exact path="/" component={Reverse} />
<Route
path="/result"
component={Result}
dataToRender={dataToRender}
/>
</>
</Switch>
<Footer />
</React.Fragment>
);
}
}
Here at Result component i tried moving informData and the const dataToRender but didn't work.
class Result extends Component {
render() {
return (
<div className="section-result">
<div className="container--result">
<h1 className="heading-primary--result">Results</h1>
<p className="paragraph--results">
Look at the result below to see the details of the person you're
searched for.
</p>
</div>
{this.props.dataToRender}
<TryAgain />
</div>
);
}
}
I just want to get the result on the result component, i had tried with all that i found, nothing worked.
Could someone tell me what do i have to do ? I'm sorry still learning about react
Just Modify the Route to this
<Route
path="/result"
render={() => <Result dataToRender={dataToRender} />}
/>
Since you have to inject props to a component, this is the way
Pass Route's render an arrow function with component and customProps
EDIT
If you also need the history, location, search props that are passed by default when we use <Route path="/" component={Something} /> then pass them explicitly
<Route
path="/result"
render = {(props) => <Result dataToRender={dataToRender} {...props} /> }
/>

Resources