admin管理员组

文章数量:1394217

How can I pass something from the React context into my Redux Saga function?

I have a context value that I could retrieve in the react ponents with something like this

const { connectionObject } = useMyContext();

This would allow me to get the connectionObject instance from the context.

However, I'm also using redux saga in some other parts of my application. One of these redux sagas will need to use the personObject from the context.

Since Redux-Sagas are technically just regular generator functions themselves, I can't use useMyContext() in it to retrieve the connectionObject. For example, this wouldn't work

export function* saveData() {
   const { connectionObject } = useMyContext();  // This will not work
   yield performSaveSomething(connectionObject);
}

The Redux Saga are triggered from me dispatching a redux action from within a react ponent. A possible workaround could of course be getting the connectionObject from the context in the react ponent and then dispatch the action with the connectionObject as part of the payload which then passes it to the saga. But this feels totally anti-pattern and isn't elegant especially when the payload is supposed to be updated to the state but in this case is used only to be passed over to the saga. Here's an example to illustrate the awkwardness:

// In the React ponent
export const myReactComponent = (props) => {
   const { connectionObject } = useMyContext();
   dispatch(saveDataAction({ 
      data: 'some data here to be put in the redux store',
      connection: connectionObject 
   }));
}

// In the reducer
function reducer (state, action) {
   switch (action.type) {
      case SAVE_DATA_ACTION_TYPE:
         // action.payload has 2 attributes in this case: data and connection
         // But I am throwing away the connection one here in this reducer because that is only meant for the saga. Feels weird here.
         return {
            ...state,
            data: action.payload.data.  // Instead of using the whole payload, I've to use only the "data" attribute of the payload because only this mattered to the store. I can't use the whole payload because it has the connectionObject in there. Doesn't look elegant.
         }
   }
}

// And finally in the saga function
export function* saveData({ payload: { connection } }) {
   // Good thing here is I could finally access the connectionObject which came from the react context
   // However, it's also doesn't seem right that I'm passing the connectionObject through the payload which isn't a piece of data but as a dependency
   yield performSaveSomething(connection);
}
   

Is there a way where I can elegantly and easily pass a value retrieved from the React context and somehow pass it into my Redux-Saga function so that it can use it?

How can I pass something from the React context into my Redux Saga function?

I have a context value that I could retrieve in the react ponents with something like this

const { connectionObject } = useMyContext();

This would allow me to get the connectionObject instance from the context.

However, I'm also using redux saga in some other parts of my application. One of these redux sagas will need to use the personObject from the context.

Since Redux-Sagas are technically just regular generator functions themselves, I can't use useMyContext() in it to retrieve the connectionObject. For example, this wouldn't work

export function* saveData() {
   const { connectionObject } = useMyContext();  // This will not work
   yield performSaveSomething(connectionObject);
}

The Redux Saga are triggered from me dispatching a redux action from within a react ponent. A possible workaround could of course be getting the connectionObject from the context in the react ponent and then dispatch the action with the connectionObject as part of the payload which then passes it to the saga. But this feels totally anti-pattern and isn't elegant especially when the payload is supposed to be updated to the state but in this case is used only to be passed over to the saga. Here's an example to illustrate the awkwardness:

// In the React ponent
export const myReactComponent = (props) => {
   const { connectionObject } = useMyContext();
   dispatch(saveDataAction({ 
      data: 'some data here to be put in the redux store',
      connection: connectionObject 
   }));
}

// In the reducer
function reducer (state, action) {
   switch (action.type) {
      case SAVE_DATA_ACTION_TYPE:
         // action.payload has 2 attributes in this case: data and connection
         // But I am throwing away the connection one here in this reducer because that is only meant for the saga. Feels weird here.
         return {
            ...state,
            data: action.payload.data.  // Instead of using the whole payload, I've to use only the "data" attribute of the payload because only this mattered to the store. I can't use the whole payload because it has the connectionObject in there. Doesn't look elegant.
         }
   }
}

// And finally in the saga function
export function* saveData({ payload: { connection } }) {
   // Good thing here is I could finally access the connectionObject which came from the react context
   // However, it's also doesn't seem right that I'm passing the connectionObject through the payload which isn't a piece of data but as a dependency
   yield performSaveSomething(connection);
}
   

Is there a way where I can elegantly and easily pass a value retrieved from the React context and somehow pass it into my Redux-Saga function so that it can use it?

Share Improve this question edited Dec 8, 2021 at 10:54 Carven asked Dec 8, 2021 at 5:52 CarvenCarven 15.7k30 gold badges124 silver badges185 bronze badges 3
  • Two side notes: First, based on that reducer example, it looks like you're writing your Redux logic "by hand". We strongly remend using our official Redux Toolkit package instead, which will drastically simplify your Redux code. Second, we actually remend against using sagas for most use cases. They're a great tool for plex async workflows, but most Redux apps don't need them - especially for basic data fetching scenarios. – markerikson Commented Dec 8, 2021 at 15:27
  • @markerikson Thanks for the tip! Just wondering, what would you suggest to use instead of sagas for most use cases? Would that be redux thunk instead? – Carven Commented Dec 10, 2021 at 18:08
  • Yes, we typically remend using thunks as the standard approach for API calls, or our new RTK Query data fetching API. See the "Redux Essentials" docs tutorial for examples of both of those. – markerikson Commented Dec 11, 2021 at 17:10
Add a ment  | 

1 Answer 1

Reset to default 8

My first inclination is to do what you proposed: include it in the payload of the action. The ponent knows the value, the saga needs the value, therefore it can pass the value in. This is likely the simplest solution.

Other than that... it kinda depends on the nature of the context value. Let's start with the simplest case and move to harder ones. Simplest case: There's only one copy of the context, and its value never changes. If it never changes, then it doesn't actually need to be in context. You can simply move the connectionObject into a file, export it, and then import it everywhere it's needed.


But you're using context, so it's probably a value that does change. So that brings us to the next case: There's only one copy, it's value does change, but only ponents care when it changes (the saga will just look it up when needed, and does not need to be notified). For this, i would probably have the context provider duplicate its state out to a global variable, which the saga checks.

let globalValue = {};
export const getGlobalValue = () => globalValue;

export const MyContextProvider = ({ children }) => {
  const [stateValue, setStateValue] = useState({});
  useEffect(() => {
    globalValue = stateValue;
  }, [stateValue]);

  return (
    <MyContext.Provider value={stateValue}>
      {children}
    </MyContext.Provider>
  )
}

// In the saga:
import { getGlobalValue } from './path/to/context/provider/file'

export function* saveData() {
   const connectionObject = getGlobalValue();
   yield performSaveSomething(connectionObject);
}

Next up in difficulty is where there's only one copy, the value changes, and both the ponents and the sagas need to be able to listen to changes in its value. For example, maybe the saga starts, checks the value, and if it sees that the value is false it needs to sleep until the value turns true.

To support this, you're going to need to have some sort of event emitter which the saga can subscribe to. There's many existing libraries for this kind of code, our you could roll your own, something like:

let listeners = [];
export const subscribe = (callback) => {
  listeners.push(callback);
  const unsubscribe = () => {
    let i = listeners.indexOf(callback);
    listeners.splice(i, 1);
  }
  return unsubscribe;
}

export const MyContextProvider = ({ children }) => {
  const [stateValue, setStateValue] = useState({});
  useEffect(() => {
    for (const listener of [...listeners]) {
      callback(stateValue);
    }
  }, [stateValue]);
  // ...
}

And finally, the case which is the hardest: there are multiple context providers. If that's the case, then there are multiple possible values, and no way for the saga to know which one to use. No way except to tell it of course, which you would do through the action payload, which then brings us back to the original idea.

I guess it would be possible to not pass the entire object in, but just an id, and then use one of the techniques above to look up the object. But i don't see that that's any better than passing the object itself.

本文标签: javascriptHow to pass a value from React Context into a ReduxSaga functionStack Overflow