I have a context, I import it into my functional component:
import { TaskContexts } from "../../../contexts";
The context stores data and functions.
The data comes from the context and is displayed on the site.
const {
editTodo,
setEditID,
toggleTodoCompletion,
editID,
editTodoHandler,
removeTodo,
state,
text,
isEditError,
} = useContext(TaskContexts);
But!
<button onClick={() => editTodo(todo.id)}>
<img src={editIcon} alt="edit button"></img>
</button>
When I try to call the editTodo function, It fails with the following error:
Uncaught TypeError: editTodo is not a function
How to fix this error?
UPD.
Full component code
import React, { useState } from 'react';
import ACTION_TYPES from '../ToDo/reducer/actionTypes';
import RenderedTable from './RenderedTable';
import styles from './TaskList.module.scss';
import allIcon from '../../icons/all.svg';
import completedIcon from '../../icons/completed.svg';
import notCompletedIcon from '../../icons/notCompleted.svg';
import mona from '../../icons/mona.gif';
import { TODO_TASK_CHEMA } from '../../utils/validationSchemas';
import { TaskContexts } from '../../contexts';
const TaskList = props => {
const {
reducerData: [state, dispatch],
} = props;
const [editID, setEditID] = useState(null);
const [editText, setEditText] = useState(null);
const [isEditError, setIsEditError] = useState(false);
const [mode, setMode] = useState('All');
const removeTodo = id => {
dispatch({ type: ACTION_TYPES.REMOVE, id });
};
const toggleTodoCompletion = id => {
dispatch({ type: ACTION_TYPES.TOGGLE, id });
};
const editTodo = id => {
const text = editText.trim();
try {
TODO_TASK_CHEMA.validateSync({ text });
} catch (e) {
setIsEditError(true);
throw new Error(e);
}
setIsEditError(false);
setEditID(null);
dispatch({ type: ACTION_TYPES.EDIT, id, text });
setEditText(null);
};
const editTodoHandler = ({ target: { value } }) => {
setEditText(value);
};
const contextsValues = {
editID,
setEditID,
editText,
setEditText,
isEditError,
setIsEditError,
mode,
setMode,
state
};
return (
<TaskContexts.Provider value={contextsValues}>
<div className={styles.container}>
{state.todos.length === 0 ? (
<div>
<h2 className={styles.noTask}>No tasks =)</h2>
<img src={mona} alt='mona gif' />
</div>
) : (
<>
<button
className={styles.section}
onClick={() => {
setMode('All');
}}
>
<img src={allIcon} alt='all button' />- All
</button>
<button
className={styles.section}
onClick={() => {
setMode('Completed');
}}
>
<img src={completedIcon} alt='completed button' />- Completed
</button>
<button
className={styles.section}
onClick={() => {
setMode('NotCompleted');
}}
>
<img src={notCompletedIcon} alt='not completed button' />- Not
completed
</button>
<RenderedTable
editTodo={editTodo}
setEditID={setEditID}
toggleTodoCompletion={toggleTodoCompletion}
editID={editID}
editTodoHandler={editTodoHandler}
removeTodo={removeTodo}
state={state}
mode={mode}
isEditError={isEditError}
/>
</>
)}
</div>
</TaskContexts.Provider>
);
};
export default TaskList;
All functions on this component do not work. But these are functions. I don't understand why React doesn't think so.
You need to do 3 things to pass the context values successfully:
Place the Context Provider at least one level above the Consuming Component.
Create Your Context, Declare all variables and methods within the Context, and Export the Context's Provider after passing the value Prop.
Consume the Context Values by importing the useContext() hook in TaskList.jsx/TaskList.js and calling it on the Provider object.
Place the Context Provider at least one level above the Consuming Component
The reason JavaScript thinks editTodo is not a function or is undefined is that you are trying to consume it in React within the <TaskList/> component before it (<TaskList/>) is even made aware of the context. By the time <TaskList/> has been rendered by React, it is too late to pass any context values. So we need to place the context, somewhere higher up the component tree where React will be made aware of the context and its values ahead of time before rendering (and passing the context values to) child components down the tree.
To fix this, place the context provider wrapper at least one level above the component that is consuming the values of the context provider. If more than one component needs values from the provider, the best place to place the provider wrapper would be in your App.jsx/App.js or your index.jsx/index.js file.
Inside App.jsx/App.js:
import { TaskProvider } from 'path/to/context';
function App() {
<TaskProvider>
{/* All your code/rendered elements/rendered route elements go here */}
</TaskProvider>
}
export default App;
or Inside index.jsx/index.js:
import React from "react";
import ReactDOM from "react-dom";
import { ToastProvider } from "path/to/context";
import "./index.css";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<ToastProvider>
<App />
</ToastProvider>
</React.StrictMode>,
document.getElementById("root")
);
I'll show you a better way to pass those context values.
Create Your Context, Declare all variables and methods within the Context, and Export the Context's Provider after passing the value Prop:
Inside TaskContexts.jsx/TaskContexts.js:
import {useContext, createContext } from "react";
// ...All your necessary imports
// Create context first
const TaskContexts = createContext();
export const TaskProvider = ({ children }) => {
const [editID, setEditID] = useState(null);
const [editText, setEditText] = useState(null);
const [isEditError, setIsEditError] = useState(false);
const [mode, setMode] = useState('All');
const removeTodo = id => {
dispatch({ type: ACTION_TYPES.REMOVE, id });
};
const toggleTodoCompletion = id => {
dispatch({ type: ACTION_TYPES.TOGGLE, id });
};
const editTodo = id => {
const text = editText.trim();
try {
TODO_TASK_CHEMA.validateSync({ text });
} catch (e) {
setIsEditError(true);
throw new Error(e);
}
setIsEditError(false);
setEditID(null);
dispatch({ type: ACTION_TYPES.EDIT, id, text });
setEditText(null);
};
// ...and the rest of the methods
// Prepare your contextValues object here
const contextValues = {
editID,
setEditID,
// ...and the rest
};
// Notice that we have called the provider here
// so that we don't have to do it within the `App.jsx` or `index.jsx`.
// We have also passed the default values here so we can that
// we don't have to export them and pass them in `App.jsx`.
// We used component composition to create a `hole` where the rest of
// our app, i.e, `{children}` will go in and returned the
// composed component from here, i.e, `<TaskProvider/>`.
// This is so that all the preparation of the context Provider object
// gets done in one file.
return (<TaskContexts.Provider value={contextValues}>
{children}
</TaskContexts.Provider>);
};
// Now, use the context, we will export it in a function called `useTask()`
// so that we don't have to call `useContext(TaskContexts)` every time we need values from the context.
// This function will call `useContext()` for us and return the values
// in the provider available as long as we wrap our app components
// with the provider (which we have already done).
export function useTask() {
return useContext(TaskContexts);
}
Consume the Context Values by importing the useContext() hook in TaskList.jsx/TaskList.js and calling it on the Provider object.
Since we've already called useContext on the provider object, we just need to import useTask() from earlier in TaskList.jsx, run it and it will return the contextValues object which we can destructure.
import React, { useState } from 'react';
import ACTION_TYPES from '../ToDo/reducer/actionTypes';
import RenderedTable from './RenderedTable';
import styles from './TaskList.module.scss';
import allIcon from '../../icons/all.svg';
import completedIcon from '../../icons/completed.svg';
import notCompletedIcon from '../../icons/notCompleted.svg';
import mona from '../../icons/mona.gif';
import { TODO_TASK_CHEMA } from '../../utils/validationSchemas';
// Import `useTask` only.
import { useTask } from '../../contexts';
const TaskList = props => {
// Values from context
const {editID, setEditID,...} = useTask();
const {
reducerData: [state, dispatch],
} = props;
const [editID, setEditID] = useState(null);
const [editText, setEditText] = useState(null);
const [isEditError, setIsEditError] = useState(false);
const [mode, setMode] = useState('All');
const removeTodo = id => {
dispatch({ type: ACTION_TYPES.REMOVE, id });
};
const toggleTodoCompletion = id => {
dispatch({ type: ACTION_TYPES.TOGGLE, id });
};
const editTodo = id => {
const text = editText.trim();
try {
TODO_TASK_CHEMA.validateSync({ text });
} catch (e) {
setIsEditError(true);
throw new Error(e);
}
setIsEditError(false);
setEditID(null);
dispatch({ type: ACTION_TYPES.EDIT, id, text });
setEditText(null);
};
const editTodoHandler = ({ target: { value } }) => {
setEditText(value);
};
return (
<div className={styles.container}>
{/*...everything else */}
<RenderedTable
editTodo={editTodo}
setEditID={setEditID}
toggleTodoCompletion={toggleTodoCompletion}
editID={editID}
editTodoHandler={editTodoHandler}
removeTodo={removeTodo}
state={state}
mode={mode}
isEditError={isEditError}
/>
</>
)}
</div>
);
};
export default TaskList;
In summary, scope everything about the context object to its own component, within its own file, export it and wrap all the children components in the root component (or wrap the root component itself), and call useContext() on the provider object in the component that needs the context values.
My application renders dynamic tiles by user and the tiles need to be re-arranged based on external configuration, StyleWrapper need to be a common container component that can be used in other projects too. Following is our UI component structure:
<StyleWrapper>
<Header />
<Content>
<PortletTiles />
</Content>
</StyleWrapper>
Given above structure, I have a function in StyleWrapper.tsx called arrangeTiles() which arranges tiles based on external configuration.
My question is how to call arrangeTiles() function from child component PortletTiles.tsx
StyleWrapper.tsx --> Parent Wrapper Container component
function StyleWrapper(props:any) {
let arrangeTiles= () => {
// Arrange Tile logic
};
return (
<div id="cnplStyleWrapper">
{props.children}
</div>
);
}
export default StyleWrapper;
PortletTiles.tsx --> Child component
function PortletTiles(props:any) {
let addNewTile= () => {
// Some logic to render tile here.. then
// How to call parent container's arrangeTiles function?
};
return (
<div>
<button onClick={addNewTile}>Add Tile</button>
</div>
);
}
export default PortletTiles;
You can create a context and pass the function or any other props as value. useContext can be used to consume the passed value from the provider.
type ContextValue = {
arrangeTiles: () => void;
};
const Context = createContext<ContextValue>({
arrangeTiles: () => {}
});
const StyleWrapper: FC = (props) => {
let arrangeTiles = () => {
// Arrange Tile logic
alert("Tiles have been arranged!");
};
return (
<Context.Provider value={{ arrangeTiles }}>
<div id="cnplStyleWrapper">{props.children}</div>
</Context.Provider>
);
};
const PortletTiles: FC = (props) => {
const { arrangeTiles } = useContext(Context);
let addNewTile = () => {
arrangeTiles();
};
return (
<div>
<button onClick={addNewTile}>Add Tile</button>
</div>
);
};
export default function App() {
return (
<StyleWrapper>
<PortletTiles />
</StyleWrapper>
);
}
If your app already uses redux, then you can move arrangeTiles to the reducer and dispatch actions from the components.
The easiest way to go about this would be to use context API. This provides you with the ability to have state that you can reference and manipulate across different components. You can reference the docs for context API here.
import { createContext } from "react";
const AppContext = createContext();
const AppProvider = (props) => {
const arrangeTiles = () => {
// place function code here
};
return (
<AppContext.Provider value={{arrangetiles}}>
{props.children}
</SiteContext.Provider>
);
};
const AppConsumer = AppContext.Consumer;
export { AppContext, AppConsumer };
export default AppProvider;
Wrap your app component in the AppProvider
In the child component, you would need to import the rearrangeTiles function and then you can use it:
import { useContext } from "react";
const { rearrangeTiles } = useContext(AppContext);
I've created a custom context hook - and I'm struggling to figure out how to pass values to its provider during testing.
My hook:
import React, { createContext, useContext, useState } from 'react';
const Context = createContext({});
export const ConfigurationProvider = ({ children }) => {
// Use State to keep the values
const [configuration, setConfiguration] = useState({});
// pass the value in provider and return
return (
<Context.Provider
value={{
configuration,
setConfiguration,
}}
>
{children}
</Context.Provider>
);
};
export const useConfigurationContext = () => useContext(Context);
export const { Consumer: ConfigurationConsumer } = Context;
This is how it's used in the application:
function App() {
return (
<ConfigurationProvider>
<div className="app">
<ComponentA />
</div>
</ConfigurationProvider>
);
}
And in ComponentA:
const ComponentA = () => {
// Get configuration
const configuration = useConfigurationContext();
return (
<div>{JSON.stringify(configuration)}</div>
)
}
This all works fine - considered that I'm calling setConfiguration from another component and set an object. Now for the testing part:
import React, { Component, createContext } from 'react';
import { render, waitFor } from '#testing-library/react';
import ComponentA from 'componentA';
const config = {
propertyA: 'hello',
};
test('renders the config', async () => {
const ConfigurationContext = createContext();
const { queryByText } = render(
<ConfigurationContext.Provider value={config}>
<ComponentA />
</ConfigurationContext.Provider>
);
expect(queryByText('hello')).toBeInTheDocument();
});
This doesn't work - I'm expecting the value that I'm sending in would be rendered in the div, but the context is an empty object. What am I doing wrong?
Thanks to Carle B. Navy I got the reason why it doesn't work. For other people two wonder what the solution is I fixed it by doing the following:
In my context hook, I changed the last line to export the provider as well:
export const { Consumer: ConfigConsumer, Provider: ConfigProvider } = Context;
Then in my test case, instead of creating a new context, I import the ConfigProvider at the top, and then:
const { queryByText } = render(
<ConfigProvider value={config}>
<ComponentA />
</ConfigProvider>
);
Thanks for helping me solve this and hope this helps someone else.
I am relatively new to react and apologies for any terms that dont fit the jargon.
I am trying to test a prototype method of a connected component which consists of a ref variable, as below:
app.js
export class Dashboard extends React.Component { // Exporting here as well
constructor(props) {
this.uploadFile = React.createRef();
this.uploadJSON = this.uploadJSON.bind(this);
}
uploadJSON () {
//Function that I am trying to test
//Conditions based on this.uploadFile
}
render() {
return (
<div className="dashboard wrapper m-padding">
<div className="dashboard-header clearfix">
<input
type="file"
ref={this.uploadFile}
webkitdirectory="true"
mozdirectory="true"
hidden
onChange={this.uploadJSON}
onClick={this.someOtherFn}
/>
</div>
<SensorStatList />
<GraphList />
</div>
);
}
const mapStateToProps = state => ({
//state
});
const mapDispatchToProps = dispatch => ({
//actions
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Dashboard);
}
Here, SensorStatList and GraphList are functional components, also connected using redux.
After some research I have my test file to this level:
app.test.js
import { Dashboard } from '../Dashboard';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
const store = configureStore();
export const CustomProvider = ({ children }) => {
return (
<Provider store={store}>
{children}
</Provider>
);
};
describe("Dashboard", () => {
let uploadJSONSpy = null;
function mountSetup () {
const wrapper = mount(
<CustomProvider>
<Dashboard />
</CustomProvider>
);
return {
wrapper
};
}
it("should read the file", () => {
const { wrapper } = mountSetup();
let DashboardWrapper = wrapper;
let instance = DashboardWrapper.instance();
console.log(instance.ref('uploadFile')) // TypeError: Cannot read property 'ref' of null
})
Can someone help me understand why this error
console.log(instance.ref('uploadFile'))
// TypeError: Cannot read property 'ref' of null
pops up? Also, if this approach is fine? If not, what are the better options?
wrapper is CustomProvider which has no instance, and ref is supposed to work with deprecated string refs.
In case a ref should be accessed on Dashboard, it can be:
wrapper.find(Dashboard).first().instance().uploadFile.current
In case input wrapper should be accessed, it can be:
wrapper.find('input').first()
I've been trying to pass id to other component with context, but I get undefined, somewhere i'm making an error.
As I understand we should get context as props.
Any Ideas ?
import {compose,withContext} from 'recompose'
const ComponentOne = ({id}) => {
console.log(id) // cizlory7iji600149711su9vj
...
}
const Context = withContext(
{id:React.PropTypes.string},
(props) => ({id:props.id})
)
export default compose(Context)(ComponentOne)
SecondComponent.js
import {compose,getContext} from 'recompose'
const ComponentTwo = ({id}) => {
console.log(id) // undefined
...
}
const GetContext = getContext(
{id:React.PropTypes.string}
)
export default compose(GetContext)(ComponentTwo)
Context only works from parents to children by passing props down, not siblings.
Make ComponentTwo a child of ComponentOne.