I have managed to convert my class component into a function one allowing me to use useContext however I am a little stuck when it comes to using and getting the status of what I need when it's changed.
I have a component called input.js which sits in src/components/input.js
// input.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Button from '../components/button';
import { getData } from '../utils/debugger';
import { DropdownContext } from '../utils/DropdownContext';
function InputForm(props) {
const [loaded, setLoaded] = useState(false);
const [dropdown, setDropdown] = useState('RTS');
const [value, setValue] = useState('');
const [data, setData] = useState([]);
function queryData(dropdown) {
return axios({
method: 'GET',
url: `http://xxx/debugger/${dropdown}`,
})
.then((res) => res.data.map((k) => k['key']['name']))
.catch((err) => console.error(err));
}
const handleChange = (event) => {
setValue(event.target.value);
};
const handleDropdown = async (event) => {
const value = event.target.value;
try {
const newData = await queryData(value);
setData(newData);
setDropdown(value);
if (newData.length > 0) {
setValue(newData[0]);
}
console.log('newData = ' + JSON.stringify(newData));
} catch (ex) {
console.error('Could not get data from axios');
}
};
const handleSubmit = (event) => {
event.preventDefault();
};
useEffect(() => {
queryData(dropdown)
.then((data) => {
setData(data);
if (data && data.length > 0) {
setValue(data[0]);
}
})
.catch(() => {
setLoaded(false);
});
}, []);
return (
<form onSubmit={handleSubmit} className='flex items-center'>
<select
value={dropdown}
onChange={handleDropdown}
className='relative w-full bg-white border border-gray-300 rounded-md shadow-sm px-1 py-3 text-center cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm mr-5'>
<DropdownContext.Provider value={dropdown}>
<option value='RTS'>RTS</option>
<option value='RTB'>RTB</option>
<option value='MPC'>MPC</option>
<option value='MPC_DSP'>MPC_DSP</option>
</DropdownContext.Provider>
</select>
<select
value={value}
onChange={handleChange}
className='relative w-full bg-white border border-gray-300 rounded-md shadow-sm px-1 py-3 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm mr-5'>
{data.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
{/* {console.log('---')}
{console.log('these will be entered into the getData()')}
{console.log(`this.state.dropdown = ${dropdown}`)}
{console.log(`this.state.value = ${value}`)} */}
<Button onClick={() => getData(dropdown, value)} color='green'>
Generate
</Button>
</form>
);
}
export default InputForm;
The component works perfectly fine outside of the context. Now I want to pass the dropdown state into App.js
// App.js
import React, {useContext } from 'react';
import './index.css';
import Button from '../src/components/button';
import RTSButtons from '../src/components/rtsButtons';
import RTBButtons from '../src/components/rtbButtons';
import MPCButtons from '../src/components/mpcButtons';
import { DropdownContext } from '../src/utils/DropdownContext';
const sectionStyles = 'mt-5 border-b pb-5';
export default function App() {
const buttonState = useContext(DropdownContext);
console.log(buttonState);
if (buttonState === 'RTS') {
console.log('RTS');
return <RTSButtons />;
} else if (buttonState === 'RTB') {
console.log('RTB');
return <RTBButtons />;
} else if (buttonState === 'MPC') {
console.log('MPC');
return <MPCButtons />;
}
return (
<div>
<section id='response' className={`flex justify-between ${sectionStyles}`}>
<div>
<Button color='red'>
<a href='http://exchange-debugger' target='_blank' rel='noreferrer'>
create a capture
</a>
</Button>
<Button onClick={() => console.log('Feedback was giving')} color='purple'>
<a
href='https://docs.google.com/forms/d/e/1FAIpQLSfzebOfAeXqGLqAp5E1l2fW1nTqSzYRwpqKG56HPXey9GQLcA/viewform'
target='_blank'
rel='noreferrer'>
feedback
</a>
</Button>
</div>
</section>
<section>{buttonState}</section>
</div>
);
}
I have a util file which is the following:
import { createContext } from 'react';
export const DropdownContext = createContext('RTS');
I now don't really know where to place my <DropdownContext.Provider value={dropdown}> To get the correct value so I'm able to pass it over to App.js. I have seen some tutorials that shows it being placed around other components...where as I just want to pass a state into another file of have that state avaiable globally.
Any help would be great, i feel I am very close but so far.
Here's a generic recipe for useContext. Say you have a file context.js:
import { createContext, useContext, useState } from 'react';
// No need to export this, we'll create custom hook around it
const SomeCtx = createContext();
// Custom hook, returns array with state, setter
export const useSomeCtx = () => useContext(SomeCtx);
// Convenience hook so you don't have to destructure
// everywhere, returns read-only state
export const useSomeCtxState = () => {
const [state] = useContext(SomeCtx);
return state;
};
// Provider, goes in render tree above components where you
// import/use the accessor hooks above.
export const SomeCtxProvider = ({children, init}) => {
const myCtx = useState(init); // [myCtxState, setMyCtxState]
return <SomeCtx.Provider value={myCtx}>{children}</SomeCtx.Provider>;
};
Then in your index.js:
import {SomeCtxProvider} from './context.js';
// other stuff
// Setting at the root makes the context globally visible, you
// may want to apply the principle of least privilege
// and put this further down in the render tree.
ReactDOM.render(<SomeCtxProvider><App /></SomeCtxProvider>, someDOMElement);
Then in your App.js
import {useSomeCtx} from './context.js';
function App() {
const [state, setState] = useSomeCtx(); // or state = useSomeCtxState()
// whatever
}
Now you can make state changes just like you normally would, and any component that uses the hooks you're providing will re-render and get the latest state. You can wire the setter to whatever event listener(s) are required (like a click on your button).
Note that unlike the older pattern where you kept your entire app state in one huge object, you aren't limited to one context. You can have different contexts with their own custom hooks per the pattern above and have them be available at any point in the render tree below where you put the provider (which in the example I gave is at the root, so everywhere in that case).
Also note that this is pretty short and sweet for how powerful it is, any component in your entire app can access this just by importing and using the custom hook defined above and will automatically re-render if it changes. You may want to be more careful about handing out the setter, which is why I included the read-only hook: global variables are evil. Although if you have an app that complex you should probably be using useReducer and dispatching actions rather than useState.
It's better to apply context API for consuming components
Take a look at your Submit action. there are 2 approaches to pursue.
onSubmit on form element
onClick on button element inside a form through type="submit"
Seems you're not sending anything after a submission using one of the above-mentioned methods. check it out.
You can also just pass the state up via a state changing function
and useState in your App.js. Then you can pass the state down to other components.
As an above picture, react never trigger the changes if you put the whole object into a new object.
Make sure you trigger the changes so that react think there are some changes and react trigger change to Context API.
I highly suggest creating a new object and adding one by one inside that new object and then finally adding to Context.
As you see in the above snap, I used two different files and get context from another file and update that context from another file.
Related
I have created a custom react hook that allows users to toggle the color theme of my application (i.e. 'light' or 'dark'), and the state change is not causing the components that call the hook to re-render.
The hook is called useColorTheme and it's defined as follows:
import { useEffect } from "react";
import useLocalStorage from "./useLocalStorage";
export default function useColorTheme() {
const [colorTheme, setColorTheme] = useLocalStorage("color-theme", "light");
useEffect(() => {
const className = "dark";
const bodyClass = window.document.body.classList;
colorTheme === "dark"
? bodyClass.add(className)
: bodyClass.remove(className);
}, [colorTheme]);
return [colorTheme, setColorTheme];
}
As you can see, this hook calls another hook called useLocalStorage, which allows the state to persist on refresh
I got this hook from usehooks.com, and it's defined as:
import { useState } from "react";
const PREFIX = "my-app-";
export default function useLocalStorage(key: string, initialValue: string) {
const prefixedKey = PREFIX + key;
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(prefixedKey);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
function setValue(value: unknown) {
try {
/**
* Don't fully understand function typing here
*/
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(prefixedKey, JSON.stringify(valueToStore));
}
} catch (error) {
console.log(error);
}
}
return [storedValue, setValue];
}
The colorTheme value is successfully being stored in localStorage, and the hook works on the initial application load, but I am having issues in components that call the useColorTheme hook, like in this DrawerContainer example:
export default function DrawerContainer() {
// calling the hook
const [theme, setTheme] = useColorTheme();
// component does not re-render when ThemeToggle component toggles theme state
return (
<div className="flex items-center justify-between gap-2">
<FiMenu size={30} className="lg:hidden mr-4 cursor-pointer" />
<Image
className="ml-10 mr-10 sm:mr-2 sm:ml-0 cursor-pointer"
src={
theme === "light"
? "/images/fiverr-logo.png"
: "/images/fiverr-logo-white.png"
}
alt="logo"
width={100}
height={100}
/>
{!user && <AuthButtons showUnderSmall />}
<ThemeToggle />
</div>
);
}
When the value of colorTheme is toggled in some other component in my application (such as my ThemeToggle component), the changed state is not being picked up in my DrawerContainer component, which prevents logic that reads from this state from happening.
I've verified that the state is indeed changing in my browser Dev Tools, so why is my DrawerContainer component not re-rendering?
Thank you very much in advance. Any insight is greatly appreciated.
Wen't thru your code and see the issue.
Your custom hook is independent,that means in every component it has its own state.
So for example you have two components A,B and both using hook.
when you changing theme using component A,new state is being enclosed inside of component A and not being passed down to B or other components that using hook as well.
To solve your issue you have to use Context API and use single state which will be passed down to other components using context.
Check out this https://reactjs.org/docs/context.html
This could be implemented like this(pseudo code):
1.Creating context
const themeContext = React.createContext();
2.Implementing hook which will be reused in components to catch context state.
function useTheme() {
return useContext(themeContext);
}
3.Implementing provider component which should be used at Root level so that can be utilized in all children components.
const {Provider} = themeContext;
function ThemeProvider(props) {
const [theme,setTheme] = useState(() => //grabbing initial theme);
const context = useMemo(() => ({theme, setTheme}), [theme])
return <Provider value={context}>{props.children}</Provider>
}
4.Wrapping components which will utilize your context.
function App() {
return (
<ThemeProvider>
<MyComponent />
</ThemeProvider>
)
}
Hope it helps!
I am fairly new to react, so still getting my head around the component's lifecycle.
But the problem has left me scratching my head.
For instance, I do not understand why adding "setState(10);" causes style of the "Test" component to revert to it's default value yet the <div ref={ref2}>Hi</div> maintains it's style. (see imagebelow)
I am aware that "setState(10);" will cause a re-render but why is the style of the "Test" component being reverted?
Also, please ignore the "practical use" of calling setState(10) - I am aware it is pointless as it is never used, and I am aware that using "state" as a UseEffect dependency can solve this issue. But the main issue I have is understanding why the component's style reverts to it's default value.
import React, { useEffect, useState, useRef } from "react";
export default function App() {
const [state, setState] = useState();
let ref1 = useRef();
let ref2 = useRef();
useEffect(() => {
console.log("useEffect called ", ref1.current);
ref1.current.style.backgroundColor = "red";
ref2.current.style.backgroundColor = "green";
setState(10);
// }, [state]);
}, []);
const Test = React.forwardRef((props, ref1) => {
console.log("test called - rendering webpage", ref1.current);
return (
<div ref={ref1} {...props}>
HI from Test{" "}
</div>
);
});
return (
<div className="App">
<Test ref={ref1} />
<div ref={ref2}>Hi</div>
</div>
);
}
Console output
test called - rendering webpage undefined
useEffect called <div style="background-color: red;">HI </div>
test called - rendering webpage <div style="background-color: red;">HI </div>
The reason the style is disappearing is that you've defined your Test component inside your App component. That means that every time App renders, you'll define a new component type named Test. The text of that component is identical to the previous one, but it's a new type as far as react can tell, so react is forced to unmount the old one, and mount the new one. This wipes out any changes you made to the old one.
So at the very least, you need to move Test outside of App. That way, the component is just defined once, and will not remount on every render
export default App() {
// ...
}
const Test = React.forwardRef((props, ref1) => {
// ...
})
The above should fix the reset and let you experiment with refs, but i strongly recommend that you do not use refs to style your elements. Refs are an escape hatch that's sometimes needed, but the standard way to style a component is through the style prop. If you need to change the style, then you can have a state variable and let that control the style prop.
If you manually use javascript to set ref1.current.style.backgroundColor, react has no way to know that you did this, and so can't take those changes into account. In some circumstances, react may end up overwriting your changes, or may skip making changes that it doesn't realize it needs to do.
export default function App () {
const [colored, setColored] = useState(false);
useEffect(() => {
setColored(true);
}, [])
return (
<div className="App">
<Test style={colored ? { backgroundColor: "green" } : undefined} />
<div style={colored ? { backgroundColor: "red" } : undefined}>Hi</div>
</div>
);
}
// Don't really need forwardRef anymore, but i left it in
const Test = React.forwardRef((props, ref) => {
return (
<div ref={ref} {...props}>
HI from Test
</div>
);
});
import React, { useEffect, useState, useRef } from "react";
export default function App() {
const [state, setState] = useState();
let ref1 = useRef();
let ref2 = useRef();
useEffect(() => {
console.log("useEffect called ", ref1.current);
ref1.current.style.backgroundColor = "red";
ref2.current.style.backgroundColor = "green";
setState(10);
// }, [ref.current]);
}, [state]);
const Test = React.forwardRef((props, ref1) => {
console.log("test called - rendering webpage", ref1.current);
return (
<div ref={ref1} {...props}>
HI from Test{" "}
</div>
);
});
return (
<div className="App">
<Test ref={ref1} />
<div ref={ref2}>Hi</div>
</div>
);
}
The reason this is happening is once you update the state entire component gets rerendered. Your useEffect will run only once on componentDidMount hence the new ref that you get is not updated. To get rid of this you should use state as a dependency of the useEffect.
I create a context and a provider as below. As you can see, I use useState() within my provider (for state) along with functions (all passed within an object as the value prop, allows for easy destructuring whatever I need in child components).
import React, { useState, createContext } from "react";
const CountContext = createContext(null);
export const CountProvider = ({ children }) => {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
const decrementCount = () => {
setCount(count - 1);
};
return (
<CountContext.Provider value={{ count, incrementCount, decrementCount }}>
{children}
</CountContext.Provider>
);
};
export default CountContext;
I wrap my app within such a provider(s) at a higher location such as at index.js.
And consume the state using useContext() as below.
import React, { useContext } from "react";
import CountContext from "../contexts/CountContext";
import Incrementer from "./Incrementer";
import Decrementer from "./Decrementer";
const Counter = () => {
const { count } = useContext(CountContext);
return (
<div className="counter">
<div className="count">{count}</div>
<div className="controls">
<Decrementer />
<Incrementer />
</div>
</div>
);
};
export default Counter;
Everything is working just fine, and I find it easier to maintain things this way as compared to some of the other methods of (shared) state management.
CodeSandbox: https://codesandbox.io/s/react-usecontext-simplified-consumption-hhfz6
I am wondering if there is a fault or flaw here that I haven't noticed yet?
One of the key differences with other state management tools like Redux is performance.
Any child that uses a Context needs to be nested inside the ContextProvider component. Every time the ContextProvider state changes it will render, and all its (non-memoized) children will render too.
In contrast, when using Redux we connect each Component to the store, so each component will render only if the part of the state it is connect to changes.
I would like to make a component that loads actions and states dynamically so that I can reuse it in other parts of the code.
I'm trying this way but the action is firing before sending to component:
function ClientsHeader(props) {
const action = Actions.searchClients();
return (
<SearchHeader data={data} campoPesquisa={campoPesquisa} action={action}/>
);
}
export default ClientsHeader;
On children:
function SearchHeader(props) {
const dispatch = useDispatch();
const action = props.action;
return;
<div className="flex flex-1 w-full items-center justify-between">
<Input
placeholder="Search"
onChange={event => dispatch(action(event))}
/>
</div>;
}
There is a way to call a function name with variables? Because that would solve this.
Please don't be rude to my stupidity, I'm new to this :D
The error is from the line const action = Actions.searchClients();
This actually calls the function searchClients.
It should rather be const action = () => Actions.searchClients();
It's because you are calling Actions.searchClients in ClientsHeader component, you just need to pass the reference as
const action = Actions.searchClients;
Note: I think you should import the action in the component directly,
Hope it helps
I'm trying to learn to create hooks so I can re-use data that I have to change in different components.
I'm using Material UI's Tabs and need to use useTab, a custom hook to change the tab id.
import React, { useContext } from 'react';
import { ProductsContext } from './ProductsContext';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import { useTab } from '../../hooks/tab';
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue] = useTab(0);
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};
export default ProductsNav;
I know it does this with child functions in the docs, but I'm trying to not just copy and paste and do it in my own way.
Here is my custom useTab hook:
import {useState, useEffect} from 'react';
export const useTab = (selectedTab) => {
const [tabValue, setTabValue] = useState(0);
useEffect(() => {
setTabValue(selectedTab);
}, []);
return [tabValue];
}
I'm of course getting an error I can't use a hook inside of a function, but I'm confused how else to do this.
How can I change tabValue from useTabs?
The error is probably here:
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
You're violating one of the primary Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function.
The reason for this rule is a bit complex but it basically boils down to the idea that hooks should only be called at the top level of a React functional component because they must be guaranteed to run every time the component function is run.
Hence why you're getting an error "I can't use a hook inside of a function"...
At any rate, it is unclear why you are using a custom hook with a useEffect() here. That seems completely unnecessary - a regular useEffect() hook inside of your nav component should more than suffice:
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue, setTabValue] = useState(0);
const handleTabChange = (e, newTabValue) => {
setTabValue(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};