Unable to run the function from the context
Asked Answered
S

1

3

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.

Stallion answered 1/6, 2022 at 20:9 Comment(6)
Please add relevant code as context provider and point of wrapping the app by the providerSitsang
Likely because editTodo isn't a function. It may be fixable by making it a function.Pisciform
@HagaiHarari, added component codeStallion
@DaveNewton, editTodo - function) I added component codeStallion
... It's the context that defines editTodo, not the component. At least based on your first snippet--in the second snippet it's something else. Which is it?Pisciform
There is no editTodo property/value passed in the context value, so the error is correct since undefined is not a function. If you want context consumers to be able to access an editTodo callback function then the editTodo function needs to be passed into the context value.Subrogation
A
8

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.

Appliance answered 2/6, 2022 at 9:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.