Trying to get along with React new Hooks and ActionCable, but stuck with the problem that I can't get the right data in Rails when trying to send state.
I've tried to use send() method immediately after doing setState() and send my updated data, but for some reason, the data which received on the Rails part is old.
For example, if I put "Example" to the input I'll see "{"data"=>"Exampl"} on the Rails side. I suppose the data update the state later than my request goes.
If I send() value from e.target.value everything works fine
Therefore I've tried to use new useEffect() hook and send data there. But I get only data when rendering the page. Afterward, I don't get anything and sometimes get error RuntimeError - Unable to find subscription with an identifier. Seems like effect hook sends data too early or something.
I'm pretty new to Hooks and WebSockets. Would love to get any help here. I can share Rails code, but there is only a receiver and nothing else.
First exmaple:
import React, { useState, useEffect } from "react"
import ActionCable from 'actioncable'
function Component(props) {
const [data, setData] = useState("");
const cable = ActionCable.createConsumer('ws://localhost:3000/cable');
const sub = cable.subscriptions.create('DataChannel');
const handleChange = (e) => {
setData(e.target.value)
sub.send({ data });
}
return (
<input value={data} onChange={handleChange}/>
)
}
Tried to useEffect and move send() there:
useEffect(() => {
sub.send({ data });
}, [data]);
I'd love to find a way to correctly use React and ActionCable. And use hooks if it's possible.
I was trying an approach similar to Oleg's but I could not setChannel inside the action cable create subscription callback. I had to setChannel outside of the callback but within the useEffect hook. Below is the solution that worked for me.
create consumer in index.js and provide the consumer through Context to App.
index.js
import React, { createContext } from 'react'
import actionCable from 'actioncable'
... omitted other imports
const CableApp = {}
CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')
export const ActionCableContext = createContext()
ReactDOM.render(
<Router>
... omitted other providers
<ActionCableContext.Provider value={CableApp.cable}>
<App />
</ActionCableContext.Provider>
</Router>,
document.getElementById('root')
)
Use the cable context in your child component and create subscription in useEffect hooks; unsubscribe in clean up
import React, { useState, useEffect, useContext } from 'react'
import { useParams } from 'react-router-dom'
... omitted code
const [channel, setChannel] = useState(null)
const { id } = useParams()
const cable = useContext(ActionCableContext)
useEffect(() => {
const channel = cable.subscriptions.create(
{
channel: 'MessagesChannel',
id: id,
},
{
received: (data) => {
receiveMessage(data)
},
}
)
setChannel(channel)
return () => {
channel.unsubscribe()
}
}, [id])
const sendMessage = (content) => {
channel.send(content)
}
You can register your cable at root component like that:
import actionCable from 'actioncable';
(function() {
window.CableApp || (window.CableApp = {});
CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')
}).call(this);`
so it will be available as global variable;
and then in any component where you want to create channel and send data:
const [channel, setChannel] = useState(null);
useEffect(() => {
CableApp.cable.subscriptions.create(
{
channel: 'YourChannelName',
},
{
initialized() {
setChannel(this)
},
},
);
}, []);
return <button onClick={() => channel.send(some_data)} >Send counter</button>
Your problem is here:
const handleChange = (e) => {
setData(e.target.value)
sub.send({ data });
}
setData is like setState in that the state is only updated after the render i.e. after the function has exited. You are sending the current data not the new data. Try this:
const handleChange = (e) => {
const newData = e.target.value;
setData(newData)
sub.send({ data: newData });
}
Related
I am following along in a React course on Udemy. In this module, we have a simple task app to demonstrate custom hooks. I've come across a situation where the "task" state is being managed in the App.js file, the "useHttp" custom hook has a function "fetchTasks" which accepts "transformTasks" as a parameter when called inside App.js. The issue I am having is that "tranformTasks" manipulates the "tasks" state inside App.js, but it is actually being called and executed inside the "useHttp" custom hook. Would really love some help understanding the mechanism for how this works. How can the state be manipulated while called from another file without the state being passed in? The code does work as intended. Here's the github link to the full app, and below are the two relevant files: https://github.com/yanichik/react-course/tree/main/full-course/custom-hooks-v2
Here is the App.js file:
import React, { useEffect, useMemo, useState } from "react";
import Tasks from "./components/Tasks/Tasks";
import NewTask from "./components/NewTask/NewTask";
import useHttp from "./custom-hooks/useHttp";
function App() {
// manage tasks state here at top level
const [tasks, setTasks] = useState([]);
const myUrl = useMemo(() => {
return {
url: "https://react-http-104c4-default-rtdb.firebaseio.com/tasks.json",
};
}, []);
const { isLoading, error, sendRequest: fetchTasks } = useHttp();
useEffect(() => {
// func transforms loaded data to add id (firebase-generated), push to loadedTasks, then
// push to tasks state
const transformTasks = (taskObj) => {
let loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
};
fetchTasks(myUrl, transformTasks);
// if you add fetchTasks as a dependency this will trigger a re-render each time states
// are set inside sendRequest (ie fetchTasks) and with each render the custom hook (useHttp)
// will be recalled to continue the cycle. to avoid this, wrap sendRequest with useCallback
}, [fetchTasks, myUrl]);
const addTaskHandler = (task) => {
setTasks((prevTasks) => prevTasks.concat(task));
};
return (
<React.Fragment>
<NewTask onEnterTask={addTaskHandler} />
<Tasks
items={tasks}
loading={isLoading}
error={error}
onFetch={fetchTasks}
/>
</React.Fragment>
);
}
export default App;
And here is the "useHttp" custom hook:
import { useState, useCallback } from "react";
// NOTE that useCallback CANNOT be used on the top level function
function useHttp() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const sendRequest = useCallback(async (httpConfig, applyFunction) => {
setIsLoading(true);
setError(false);
try {
const response = await fetch(httpConfig.url, {
method: httpConfig.method ? httpConfig.method : "GET",
headers: httpConfig.headers ? httpConfig.headers : {},
body: httpConfig.body ? JSON.stringify(httpConfig.body) : null,
});
// console.log("response: " + response.method);
if (!response.ok) {
throw new Error("Request failed!");
}
const data = await response.json();
applyFunction(data);
// console.log("the formatted task is:" + applyFunction(data));
} catch (err) {
setError(err.message || "Something went wrong!");
}
setIsLoading(false);
}, []);
return { sendRequest, isLoading, error };
}
export default useHttp;
Sounds like you're learning from a decent course. The hook is using a technique called "composition". It knows you'll want to do some processing on the data once it has been fetched and let's you pass in (the applyFunction variable) your own snippet of code to do that processing.
Your snippet of code is just a function, but all parties agree on what parameters the function takes. (This is where using typescript helps catch errors.)
So you pass in a function that you write, and your function takes 1 parameter, which you expect will be the data that's downloaded.
The useHttp hook remembers your function and once it has downloaded the data, it calls your function passing in the data.
If you've used some of your own variables within the function you pass to the hook, they get frozen in time ... sort-of. This can of worms is a topic called 'closures' and I'm sure it will come up in the course if it hasn't already.
Im migrating over from Angular5. In angular5 i was using services, now im trying to use react.
Im trying to call a function from globalService.js, and do something with it in app.js.
so far i got this error:
TypeError: _globalService__WEBPACK_IMPORTED_MODULE_5__.default.PerformRequest is not a function
test data:
app contains:
import doSomething from "./globalService";
console.log("doing a request",doSomething.PerformRequest('test'));
globalservice:
const PerformRequest = function(data) {
console.log("data is:",data);
}
export default PerformRequest;
test data end
Ideally im trying to translate my angular code to react code.
here is my angular old code:
app call:
click() {
this.GlobalService.PerformRequest('/crime').pipe(takeUntil(this._destroyed$)).subscribe((data:any) => {
DO SOMETHING WITH DATA HERE
});
}
globalservice call:
PerformRequest(params) {
console.log(this.baseURL + params);
return this.http.get(this.baseURL + params,this.options)
.pipe(
//catchError(this.handleError)
);
}
Q1: What am i doing wrong in my first example above?
Q2: How would i rewrite my angular data to react language so i can use it to call from a component the same was as im calling it?
Q1: Your import is not correct, it should be
import PerformRequest from './globalService';
if you keep the globalService the way it's currently return. If you do want something like a service you could export an object containing all the methods.
export default {
PerformRequest,
};
Q2:
I'd suggest you look into hooks to reproduce what you already have as a service.
You could do something like that:
import { useEffect, useState } from 'react';
// define a hook
const useFetchUrl = (url) => {
// useState are used to store some state
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState();
// useEffect is done for side effect
useEffect(() => {
fetch(url)
.then(resp => resp.json())
.then(data => setData(data);
}, [url];
return {
isLoading,
data,
};
}
const Component = () => {
const { loading, data } = useFetchUrl('/some/url');
if (!loading) {
return <div>Loading...</div>;
}
return <pre>{JSON.stringify(data)}</pre>;
};
swr is a nice library that provides you with hooks you could use to do the same kind of thing without having to write your own custom hooks and dealing with some more complicated cases.
My folder structure:
|--App
|--Components
|--PageA.js
|--PageB.js
|--PageC.js
|--common-effects
|--useFetching.js
I am refactoring my code to fetch data from API, using react hooks.
I want to dispatch an action from useEffect in useFetching.js that is intercepted by saga middleware. The action should be dispatched only when the components(PageA, PageB, PageC) mount.
I am using redux, react-redux and redux-saga.
PageA.js:
function(props) {
useFetching(actionParams)
//....//
}
Similar code for PageB and PageC components.
I have abstracted the reusable code to fetch data in useFetching Custom hook.
useFetching.js
const useFetching = actionArgs => {
useEffect( () => {
store.dispatch(action(actionArgs)); // does not work
})
}
I don't know how to access redux dispatch in useFetching. I tried it with useReducer effect, but the sagas missed the action.
Version using react-redux hooks:
You can even cut out the connect function completely by using useDispatch from react-redux:
export default function MyComponent() {
useFetching(fetchSomething);
return <div>Doing some fetching!</div>
}
with your custom hook
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
Edit: removed dispatch from custom hook as suggested by #yonga-springfield
Note: React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
You would need to pass either bound action creators or a reference to dispatch to your hook. These would come from a connected component, same as you would normally use React-Redux:
function MyComponent(props) {
useFetching(props.fetchSomething);
return <div>Doing some fetching!</div>
}
const mapDispatch = {
fetchSomething
};
export default connect(null, mapDispatch)(MyComponent);
The hook should then call the bound action creator in the effect, which will dispatch the action accordingly.
Also, note that your current hook will re-run the effect every time the component is re-rendered, rather than just the first time. You'd need to modify the hook like this:
const useFetching = someFetchActionCreator => {
useEffect( () => {
someFetchActionCreator();
}, [])
}
This is just to bring some optimization to #Alex Hans' answer.
As per the documentation here. A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
With this in mind, we need not send a reference to the dispatch function to the useFetching hook as a parameter but rather, simply not send it and rather simply use it from within the useFetching hook with the appropriate imports.
Here's an excerpt of what I mean.
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
I can't ascertain this example will fit without errors in your codebase in your case but just trying to explain the idea/concept behind this post.
Hope this helps any future comer.
Alex Hans right decision with dispatch, but to eliminate request loops to api you can specify the dependence on dispatch ( I used Redux Toolkit )
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import { getItemsStart, getItemsSuccess, getItemsFailure } from '../features/itemsSlice'
const fetchItems = () => async dispatch => {
try {
dispatch(getItemsStart());
const { data } = await axios.get('url/api')
dispatch(getItemsSuccess(data))
} catch (error) {
dispatch(getItemsFailure(error))
}
}
const PageA = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.dataSlice)
useEffect(() => {
dispatch(fetchItems())
}, [dispatch])
return (
<ul>
{items.map(item => <li>{item.name}</li>}
</ul>
)
}
export default PageA
it is important to passed dependency parameter of dispatch in the useEffect(() => {...}, [dispatch])
useEffect(() => {
fetchData();
}, []);
async function fetchData() {
try {
await Auth.currentSession();
userHasAuthenticated(true);
} catch (e) {
if (e !== "No current user") {
alert(e);
}
}
dispatch(authentication({ type: "SET_AUTHING", payload: false }));
}
I need to write a test with the following steps:
get user data on mount
get project details if it has selectedProject and clientId when they change
get pages details if it has selectedProject, clientId, and selectedPages when they change
render Content inside Switch
if doesn't have clientId, Content should return null
if doesn't have selectedProject, Content should return Projects
if doesn't have selectedPages, Content should return Pages
else Content should render Artboard
And the component looks like this:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUserData } from "../../firebase/user";
import { selectProject } from "../../actions/projects";
import { getItem } from "../../tools/localStorage";
import { getProjectDetails } from "../../firebase/projects";
import { selectPages } from "../../actions/pages";
import Pages from "../Pages";
import Projects from "../Projects";
import Artboard from "../Artboard";
import Switch from "../Transitions/Switch";
import { getUserId, getClientId } from "../../selectors/user";
import { getSelectedProject } from "../../selectors/projects";
import { getSelectedPages, getPagesWithDetails } from "../../selectors/pages";
import { getPagesDetails } from "../../firebase/pages";
const cachedProject = JSON.parse(getItem("selectedProject"));
const cachedPages = JSON.parse(getItem("selectedPages"));
const Dashboard = () => {
const dispatch = useDispatch();
const userId = useSelector(getUserId);
const clientId = useSelector(getClientId);
const selectedProject = useSelector(getSelectedProject) || cachedProject;
const selectedPages = useSelector(getSelectedPages) || cachedPages;
const pagesWithDetails = useSelector(getPagesWithDetails);
useEffect(() => {
dispatch(
getUserData(userId)
);
cachedProject && selectProject(cachedProject);
cachedPages && selectPages(cachedPages);
}, []);
useEffect(() => {
if (selectedProject && clientId) {
dispatch(
getProjectDetails(
clientId,
selectedProject
)
);
}
}, [selectedProject, clientId]);
useEffect(() => {
if (selectedPages && selectedProject && clientId) {
const pagesWithoutDetails = selectedPages.filter(pageId => (
!Object.keys(pagesWithDetails).includes(pageId)
));
dispatch(
getPagesDetails(
selectedProject,
pagesWithoutDetails
)
);
}
}, [selectedPages, selectedProject, clientId]);
const Content = () => {
if (!clientId) return null;
if (!selectedProject) {
return <Projects key="projects" />;
}
if (!selectedPages) {
return <Pages key="pages" />;
}
return <Artboard key="artboard" />;
};
console.log("Update Dashboard")
return (
<Switch>
{Content()}
</Switch>
);
};
Where I use some functions to fetch data from firebase, some to dispatch actions, and some conditionals.
I'm trying to get deep into testing with Jest and Enzyme. When I was searching for testing approaches, testing useEffect, variables, and conditions, I haven't found anything. All I saw is testing if a text changes, if a button has get clicked, etc. but what about testing components which aren't really changing anything in the DOM, just loading data, and depending on that data, renders a component?
What's the question here? What have you tried? To me it seems pretty straightforward to test:
Use Enzymes mount or shallow to render the component and assign that to a variable and wrap it in a store provider so it has access to a redux store.
Use jest.mock to mock things you don't want to actually want to happen (like the dispatching of actions) or use something like redux-mock-store.
Use that component ".find" to get the actual button you want.
Assert that, given a specific redux state, it renders correctly.
Assert that actions are dispatched with the proper type and payload at the proper times.
You may need to call component.update() to force it to rerender within the enzyme test.
Let me know if you have more specific issues.
Good luck!
My folder structure:
|--App
|--Components
|--PageA.js
|--PageB.js
|--PageC.js
|--common-effects
|--useFetching.js
I am refactoring my code to fetch data from API, using react hooks.
I want to dispatch an action from useEffect in useFetching.js that is intercepted by saga middleware. The action should be dispatched only when the components(PageA, PageB, PageC) mount.
I am using redux, react-redux and redux-saga.
PageA.js:
function(props) {
useFetching(actionParams)
//....//
}
Similar code for PageB and PageC components.
I have abstracted the reusable code to fetch data in useFetching Custom hook.
useFetching.js
const useFetching = actionArgs => {
useEffect( () => {
store.dispatch(action(actionArgs)); // does not work
})
}
I don't know how to access redux dispatch in useFetching. I tried it with useReducer effect, but the sagas missed the action.
Version using react-redux hooks:
You can even cut out the connect function completely by using useDispatch from react-redux:
export default function MyComponent() {
useFetching(fetchSomething);
return <div>Doing some fetching!</div>
}
with your custom hook
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
Edit: removed dispatch from custom hook as suggested by #yonga-springfield
Note: React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
You would need to pass either bound action creators or a reference to dispatch to your hook. These would come from a connected component, same as you would normally use React-Redux:
function MyComponent(props) {
useFetching(props.fetchSomething);
return <div>Doing some fetching!</div>
}
const mapDispatch = {
fetchSomething
};
export default connect(null, mapDispatch)(MyComponent);
The hook should then call the bound action creator in the effect, which will dispatch the action accordingly.
Also, note that your current hook will re-run the effect every time the component is re-rendered, rather than just the first time. You'd need to modify the hook like this:
const useFetching = someFetchActionCreator => {
useEffect( () => {
someFetchActionCreator();
}, [])
}
This is just to bring some optimization to #Alex Hans' answer.
As per the documentation here. A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
With this in mind, we need not send a reference to the dispatch function to the useFetching hook as a parameter but rather, simply not send it and rather simply use it from within the useFetching hook with the appropriate imports.
Here's an excerpt of what I mean.
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
I can't ascertain this example will fit without errors in your codebase in your case but just trying to explain the idea/concept behind this post.
Hope this helps any future comer.
Alex Hans right decision with dispatch, but to eliminate request loops to api you can specify the dependence on dispatch ( I used Redux Toolkit )
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import { getItemsStart, getItemsSuccess, getItemsFailure } from '../features/itemsSlice'
const fetchItems = () => async dispatch => {
try {
dispatch(getItemsStart());
const { data } = await axios.get('url/api')
dispatch(getItemsSuccess(data))
} catch (error) {
dispatch(getItemsFailure(error))
}
}
const PageA = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.dataSlice)
useEffect(() => {
dispatch(fetchItems())
}, [dispatch])
return (
<ul>
{items.map(item => <li>{item.name}</li>}
</ul>
)
}
export default PageA
it is important to passed dependency parameter of dispatch in the useEffect(() => {...}, [dispatch])
useEffect(() => {
fetchData();
}, []);
async function fetchData() {
try {
await Auth.currentSession();
userHasAuthenticated(true);
} catch (e) {
if (e !== "No current user") {
alert(e);
}
}
dispatch(authentication({ type: "SET_AUTHING", payload: false }));
}