I created a HOC to handle all the logic necessary for sockets setup + handlers and wrapped my component into it passing HOC's state at the same time. I added useEffect to the wrapped component to change it's state after it gets new props from HOC. The problem is that even if it logs these props correctly in the console, it is somehow broken. The output doesn't display even after getting props, and the loading spinner is working all the time despite the fact that the loading state is set to false from the beginning. Does anyone know what may be causing this and how can I fix this?
HOC:
import React, { useState, useEffect, useContext } from "react";
import SocketContext from "../../components/sockets/socketContext";
import axios from "axios";
import { SentimentOutput } from "./../../types/outputTypes";
import { TaskLoading } from "./../../types/loadingTypes";
export default function withSocketActions(HocComponent: any) {
return (props: any) => {
const [output, setOutput] = useState({
score: undefined,
label: undefined,
});
const [loading, setLoading] = useState(false);
const contextProps = useContext(SocketContext);
useEffect(() => {
if (contextProps) {
const { socket } = contextProps;
socket.on("status", (data: any) => {
if (
data.message.status === "processing" ||
data.message.status === "pending"
) {
setLoading(true);
console.log(data);
} else if (data.message.status === "finished") {
setLoading(false);
getOutput(data.message.task_id);
console.log(data);
}
});
return () => {
socket.off("");
};
}
}, []);
const getOutput = async (id: string) => {
const response = await axios.get(`http://localhost:9876/result/${id}`);
console.log("Output: ", response.data);
setOutput(response.data);
};
return (
<>
<HocComponent props={{ ...props, output, loading }} />
</>
);
};
}
Component:
import React, { useState, FormEvent, useEffect, useContext } from "react";
import axios from "axios";
import PulseLoader from "react-spinners/PulseLoader";
import { faTag, faPoll } from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import withSocketActions from "../../components/sockets/withSocketActions";
import "../../styles/containers.scss";
import "../../styles/buttons.scss";
import "../../styles/text.scss";
function SentimentInput(props: any) {
const [input, setInput] = useState("");
const [output, setOutput] = useState({
score: "",
label: "",
});
const [loading, setLoading] = useState(false);
useEffect(() => {
setOutput({ score: props.output?.score, label: props.output?.label });
setLoading(props.loading);
console.log("OUTPUT: ", props);
}, [props]);
const getHighlightColour = (label: string | undefined) => {
if (label === "POSITIVE") return "#57A773";
else if (label === "NEGATIVE") return "#F42C04";
else return "transparent";
};
const submitInput = async (input: string) => {
let formData = new FormData();
formData.set("text", input);
if (props.model) formData.set("model", props.model);
const response = await axios.post(
`http://localhost:9876/run/sentiment_analysis`,
formData
);
console.log("RESPONSE: ", response.data.id);
};
const handleSubmit = async (e: FormEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log(input);
const result = await submitInput(input);
};
return (
<div className="inputContainer">
<div style={{ width: "100%", height: "100%", justifyContent: "center" }}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
rows={25}
className={"inputArea"}
readOnly={loading}
style={{
boxShadow: `0 0 12px 2px ${getHighlightColour(
output && output.label
)}`,
}}
autoFocus
placeholder={"Insert text for evaluation"}
/>
<button
className={"submitInputButton"}
onClick={(e) => handleSubmit(e)}
>
<div className={"topButtonText"}>Evaluate</div>
</button>
<PulseLoader loading={loading} color={"white"} size={6} />
{output &&
output.score !== undefined &&
output.label !== undefined &&
!loading && (
<div
style={{
marginTop: "10px",
display: "flex",
justifyContent: "center",
}}
>
<FontAwesomeIcon
icon={faTag}
size={"lg"}
color={"#f0edee"}
style={{ paddingRight: "5px" }}
/>
<div
className={
output && output.label === "POSITIVE"
? "outputInfo labelPositive"
: "outputInfo labelNegative"
}
>
{output.label}
</div>
<FontAwesomeIcon
icon={faPoll}
size={"lg"}
color={"#f0edee"}
style={{ paddingRight: "5px" }}
/>
<div className={"outputInfo"}>{output.score}</div>
</div>
)}
</div>
</div>
);
}
export default withSocketActions(SentimentInput);
Writing #Drew's comment as an answer
<HocComponent props={{ ...props, output, loading }} />
looks to be nesting your props in a prop named props, i.e. props.props.output
change it to-
<HocComponent {...props} output={output} loading={loading} />
Related
I am implementing a provider which helps me to have the state of my user in different views, the main function of this provider is to render different one or the other stack navigator depending on whether the variable is full or empty, this in order to be able to generate two groups of screens depending on whether the user is authenticated or not.
Here my code:
View router.tsx:
import { NavigationContainer } from "#react-navigation/native"
import React, { useContext, useEffect, useState, useRef } from "react"
import { UserContext } from "./context/Usuario"
import AuthStack from "./routes/AuthStack"
import GeneralStack from "./routes/GeneralStack"
const Router = () => {
const { me } = useContext(UserContext)
const auth = useRef(false)
useEffect(() => {
return () => {
auth.current = me !== null
console.log("Hola")
}
}, [me])
return (
<NavigationContainer>
{auth.current ? <GeneralStack /> : <AuthStack />}
</NavigationContainer>
)
}
export default Router
Provider user,js:
import React, { useEffect, createContext, useState } from "react"
import AsyncStorage from "#react-native-async-storage/async-storage"
export const UserContext = createContext()
const UserProvider = ({ children }) => {
const [me, setMe] = useState(undefined)
const validStorage = async () => {
try {
const miSesion = await AsyncStorage.getItem("sesion")
console.log(miSesion)
setMe(JSON.parse(miSesion))
} catch (error) {
console.log(`ERROR: ${error.message}`)
}
}
useEffect(() => {
validStorage()
}, [])
return (
<UserContext.Provider value={{ me, setMe }}>
{children}
</UserContext.Provider>
)
}
export default UserProvider
GeneralStack:
import { createNativeStackNavigator } from "#react-navigation/native-stack"
import React from "react"
import TabStack from "./TabStack"
//import TabStack from "./TabStack"
const GeneralScreen = createNativeStackNavigator()
const GeneralStack = () => {
return (
<GeneralScreen.Navigator screenOptions={{ headerShown: false }}>
<GeneralScreen.Screen name="Tabs" component={TabStack} />
</GeneralScreen.Navigator>
)
}
export default GeneralStack
AuthStack:
import { createNativeStackNavigator } from "#react-navigation/native-stack"
import React from "react"
import Login from "../Screens/Login"
import Registro from "../Screens/Registro/Registro"
import SplashScreen from "../SplashScreen"
const AuthScreen = createNativeStackNavigator()
const AuthStack = () => {
return (
<AuthScreen.Navigator
initialRouteName="Splash"
screenOptions={{ headerShown: false }}>
<AuthScreen.Screen name="Login" component={Login} />
<AuthScreen.Screen name="Register" component={Registro} />
<AuthScreen.Screen name="Splash" component={SplashScreen} />
</AuthScreen.Navigator>
)
}
export default AuthStack
Login:
import React, { useState, useContext } from "react"
import {
Image,
ScrollView,
StatusBar,
Text,
TouchableOpacity,
View,
} from "react-native"
import { useNavigate } from "../../Hooks/useNavigate"
import MyTextInput from "../../components/MyTextInput"
import colors from "../../styles/colors"
import { loginStyles } from "../../styles/styles"
import { UserContext } from "../../context/Usuario"
import AsyncStorage from "#react-native-async-storage/async-storage"
export default function Login() {
const [, setIsSession] = useState(false)
const { setMe } = useContext(UserContext)
const navigate = useNavigate()
const [hidePassword, sethidePassword] = React.useState(false)
const [user] = useState({ user: "admin", password: "admin123" })
const [form, setForm] = useState({ user: "", password: "" })
const getStorage = async () => {
if (await AsyncStorage.getItem("sesion")) {
setIsSession(true)
} else {
setIsSession(false)
}
}
const signIn = async () => {
try {
console.log(user)
if (form.user === user.user && form.password === user.password) {
await AsyncStorage.setItem("sesion", JSON.stringify(form))
setMe(form)
setIsSession(true)
}
} catch (error) {
console.error(error)
}
}
const closeSesion = async () => {
await AsyncStorage.removeItem("sesion")
getStorage()
}
return (
<ScrollView contentContainerStyle={[loginStyles.container]}>
<StatusBar backgroundColor={colors.PURPLE} translucent={true} />
<View style={loginStyles.logo}>
<Image
source={require("../../recursos/images/Logo.png")}
style={{ height: 250, width: 250 }}
/>
</View>
<MyTextInput
onChangeText={(text: string) => {
setForm(state => ({ ...state, user: text }))
}}
keyboardType="email-address"
placeholder="E-mail"
/>
<MyTextInput
onChangeText={(text: string) => {
setForm(state => ({ ...state, password: text }))
}}
keyboardType={null}
placeholder="Contraseña"
bolGone={true}
secureTextEntry={hidePassword}
onPress={() => sethidePassword(!hidePassword)}
/>
<View style={loginStyles.btnMain}>
<TouchableOpacity onPress={signIn}>
<Text style={loginStyles.btntxt}>Iniciar Sesión</Text>
</TouchableOpacity>
</View>
<View style={loginStyles.btnTransparent}>
<TouchableOpacity
onPress={() => navigate({ screen: "Register" })}>
<Text
style={[loginStyles.btntxt, { color: colors.PURPLE }]}>
Registrarse
</Text>
</TouchableOpacity>
</View>
<View>
<TouchableOpacity>
<Text
style={[
loginStyles.txtTransparent,
{ textDecorationLine: "none" },
]}>
Olvide mi contraseña
</Text>
</TouchableOpacity>
</View>
</ScrollView>
)
}
Home:
import AsyncStorage from "#react-native-async-storage/async-storage"
import React, { useState } from "react"
import { Image, ScrollView, TouchableOpacity } from "react-native"
import { Calendar } from "react-native-calendars"
import { Text } from "react-native-elements"
import { useNavigate } from "../../Hooks/useNavigate"
import colors from "../../styles/colors"
const Home = () => {
const [showModal, setShowModal] = useState(false)
const [date, setDate] = useState<string>()
const navigate = useNavigate()
const [isSession, setIsSession] = useState(false)
const getStorage = async () => {
const data = await AsyncStorage.getItem("sesion")
console.log(data)
if (data) {
setIsSession(true)
} else {
setIsSession(false)
}
}
const closeSesion = async () => {
await AsyncStorage.removeItem("sesion")
getStorage()
}
return (
<ScrollView>
<TouchableOpacity onPress={() => closeSesion()}>
<Text>Hola porque soy bien molon</Text>
</TouchableOpacity>
<Image
source={require("../../recursos/images/coralio_logo.png")}
style={{
marginTop: 40,
height: 110,
width: "80%",
justifyContent: "center",
alignSelf: "center",
}}
/>
<Calendar
theme={{
selectedDayBackgroundColor: colors.BLACK,
arrowColor: colors.WHITE,
monthTextColor: colors.WHITE,
}}
style={{
backgroundColor: colors.PURPLE,
borderRadius: 10,
elevation: 4,
marginTop: 60,
margin: 10,
height: 400,
}}
onDayPress={day => {
console.log(day.dateString)
setDate(day.dateString)
setShowModal(false)
}}
onMonthChange={() => {}}
initialDate={"2023-01-16"}
minDate={new Date()
.toLocaleDateString("es-US", {
year: "numeric",
month: "2-digit",
day: "numeric",
formatMatcher: "basic",
})
.split("/")
.reverse()
.join("-")}
markedDates={{
day: {
marked: true,
dotColor: colors.WHITE,
selected: true,
selectedColor: colors.PURPLE,
},
}}
//maxDate={"2023-12-31"}
//hideExtraDays={false}
//disableArrowLeft={true}
//disableArrowRight={true}
//hideArrows={true}
//hideDayNames={true}
/>
</ScrollView>
)
}
export default Home
The problem I have is when doing the close session in home, if in login it sends me from the authstack view to generalstack, when I do the close session it doesn't send me back to login, but it does clean the state of the variable from asyncstorage.
Help :(
It looks like you're not clearing the me variable in your context when the user ends the session. I think your closeSesion method should look like this:
const closeSesion = async () => {
setMe(null)
await AsyncStorage.removeItem("sesion")
getStorage()
}
I have figured out how to pull the notifications from the database but having trouble creating a filter that allows the user that asked the question to only get notifications when their questions have been answered.
The code I wrote is getting an unmounted error on line 31 with how I'm performing the filter. Here is the error message:
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
Here is the code:
import React, { useEffect, useState } from "react";
import { makeStyles } from "#material-ui/core/styles";
import Popper from "#material-ui/core/Popper";
import NotificationsIcon from "#material-ui/icons/Notifications";
import "../Style/Header.css";
import db, { auth } from "../firebase";
const useStyles = makeStyles((theme) => ({
paper: {
border: "1px solid",
padding: theme.spacing(1),
backgroundColor: theme.palette.background.paper,
zIndex: "10",
},
}));
export default function SimplePopper() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const [notifications, setNotifications] = useState([]);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const open = Boolean(anchorEl);
const id = open ? "simple-popper" : undefined;
useEffect(() => {
let mounted = true;
db.collection("notifications")
.where(auth.askerUserId, "==", auth.currentUser.uid)
.orderBy("timestamp", "desc")
.onSnapshot((snapshot) => {
if (mounted) {
setNotifications(
snapshot.docs.map((doc) => ({
id: doc.id,
content: doc.data().content,
}))
);
}
});
return () => (mounted = false);
}, []);
return (
<div className="header__icon">
<NotificationsIcon
aria-describedby={id}
type="button"
onClick={handleClick}
/>
<Popper id={id} open={open} anchorEl={anchorEl} style={{ zIndex: 100 }}>
<div className={classes.paper}>
<ul className="notifications">
{notifications.map((notification) => (
<li key={notification.id}>{notification.content}</li>
))}
</ul>
</div>
</Popper>
</div>
);
}
You should unsubscribe when the component gets unmounted:
import React, { useEffect, useState } from "react";
import { makeStyles } from "#material-ui/core/styles";
import Popper from "#material-ui/core/Popper";
import NotificationsIcon from "#material-ui/icons/Notifications";
import "../Style/Header.css";
import db, { auth } from "../firebase";
const useStyles = makeStyles((theme) => ({
paper: {
border: "1px solid",
padding: theme.spacing(1),
backgroundColor: theme.palette.background.paper,
zIndex: "10",
},
}));
export default function SimplePopper() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const [notifications, setNotifications] = useState([]);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const open = Boolean(anchorEl);
const id = open ? "simple-popper" : undefined;
useEffect(() => {
let mounted = true;
let unsub=db.collection("notifications")
.where(auth.askerUserId, "==", auth.currentUser.uid)
.orderBy("timestamp", "desc")
.onSnapshot((snapshot) => {
if (mounted) {
setNotifications(
snapshot.docs.map((doc) => ({
id: doc.id,
content: doc.data().content,
}))
);
}
});
return () => {unsub()};
}, []);
return (
<div className="header__icon">
<NotificationsIcon
aria-describedby={id}
type="button"
onClick={handleClick}
/>
<Popper id={id} open={open} anchorEl={anchorEl} style={{ zIndex: 100 }}>
<div className={classes.paper}>
<ul className="notifications">
{notifications.map((notification) => (
<li key={notification.id}>{notification.content}</li>
))}
</ul>
</div>
</Popper>
</div>
);
}
I am fetching data from Fire store in real-time with .onSnapshot and it works great, I am receiving the data as expected. The problem is that I am receiving multiple sets of data, and the component does not wait until all the data is received before rendering.
So my question is, with my current code, is their a way in which I can wait for all sets of my data to be fetched before displaying them?
My current code is:
import React, {useEffect, useState} from 'react';
import {ActivityIndicator, Dimensions, Text, View} from 'react-native';
import firestore from '#react-native-firebase/firestore';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import FolloweringScreens from './FolloweringScreens';
import {TouchableOpacity} from 'react-native-gesture-handler';
const {width, height} = Dimensions.get('screen');
function Following({urlname, navigation}) {
const [followingData, setfollowingData] = useState([]);
// Follower counts, displayname, image
const fetchData = () => {
const dataRef = firestore().collection('usernames');
dataRef
.doc(urlname)
.collection('Following')
.onSnapshot((snapshot) => {
snapshot.forEach((doc) => {
dataRef.doc(doc.id.toLowerCase()).onSnapshot((followerDoc) => {
const data = followerDoc.data();
setfollowingData((prev) => [
...prev,
{
profileName: doc.id,
displayName: data.userName,
followerCount:
data.followers !== undefined ? data.followers : 0,
followingCount:
data.following !== undefined ? data.following : 0,
image: data.imageUrl ? data.imageUrl : null,
},
]);
});
});
});
};
useEffect(() => {
fetchData();
}, []);
return (
<>
<View
style={{
left: width * 0.04,
top: 50,
flexDirection: 'row',
alignItems: 'center',
width: '80%',
height: '4%',
marginBottom: 5,
}}>
{/* {console.log('followin', followingData)} */}
<TouchableOpacity onPress={() => navigation.openDrawer()}>
<Icon name="menu" color="#222" size={30} />
</TouchableOpacity>
<Text style={{left: width * 0.05}}>Following</Text>
</View>
{followingData === [] ? (
<ActivityIndicator size="large" color="black" />
) : (
<>
<FolloweringScreens data={followingData} />
</>
)}
</>
);
}
export default Following;
Use a state isLoading default true, then set isLoading to false once a snapshot resolves, and show a loading indicator on isLoading true and show your ui when isLoading false. Then you get updates pushed to your state and the user will see the data once it's fully loaded.
Would also use something close to this. One thing that is quite weird is that you push every snapshot change to an array, so in other words over time this array holds a history of changes of the same object. Intentional?
function Following({ urlname }) {
const [followingData, setfollowingData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Follower counts, displayname, image
const onSnapshot = useCallback((snapshot) => {
snapshot.forEach((doc) => {
dataRef.doc(doc.id.toLowerCase()).onSnapshot((followerDoc) => {
const data = followerDoc.data();
// push new document data into an array
setfollowingData((prev) => [
...prev,
{
profileName: doc.id,
displayName: data.userName,
followerCount: data.followers !== undefined ? data.followers : 0,
followingCount: data.following !== undefined ? data.following : 0,
image: data.imageUrl ? data.imageUrl : null
}
]);
// or set the new data to state, by just setting the document data
setfollowingData(data);
setIsLoading(false);
});
});
}, []);
useEffect(() => {
const dataRef = firestore().collection("usernames");
const cleanup = dataRef
.doc(urlname)
.collection("Following")
.onSnapshot(onSnapshot);
return cleanup;
}, [onSnapshot, urlname]);
return (
<>
{isLoading && <p>Loading</p>}
{!isLoading && <p>Show data {followingData.length}</p>}
</>
);
}
So I managed to fix it somehow. Thanks to Julian for the help
What I did was create an array of promises which will be executed whenever the data changes. The code is:
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Dimensions, Text, View} from 'react-native';
import firestore from '#react-native-firebase/firestore';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import FolloweringScreens from './FolloweringScreens';
import {TouchableOpacity} from 'react-native-gesture-handler';
const {width, height} = Dimensions.get('screen');
function Following({urlname, navigation}) {
const [followingData, setfollowingData] = useState();
const [loading, setLoading] = useState(true);
// Following counts, displayname, image
const fetchData = useCallback(() => {
const dataRef = firestore().collection('usernames');
dataRef
.doc(urlname)
.collection('Following')
.limit(25)
.onSnapshot((snapshot) => {
let promises = [];
snapshot.forEach((doc) => {
const promise = dataRef
.doc(doc.id.toLowerCase())
.get()
.then((followerDoc) => {
const data = followerDoc.data();
return {
profileName: doc.id,
displayName: data.displayName
? data.displayName
: data.userName,
followerCount:
data.followers !== undefined ? data.followers : 0,
followingCount:
data.following !== undefined ? data.following : 0,
image: data.imageUrl ? data.imageUrl : null,
};
});
promises.push(promise);
});
Promise.all(promises)
.then((res) => setfollowingData(res))
.then(setLoading(false));
});
}, []);
useEffect(() => {
const dataRef = firestore().collection('usernames');
const cleanup = dataRef
.doc(urlname)
.collection('Following')
.limit(25)
.onSnapshot(fetchData);
return cleanup;
// fetchData();
}, [urlname, fetchData]);
return (
<>
<View
style={styles}>
<TouchableOpacity onPress={() => navigation.openDrawer()}>
<Icon name="menu" color="#222" size={30} />
</TouchableOpacity>
<Text style={{left: width * 0.05}}>Following</Text>
</View>
{loading ? (
<ActivityIndicator size="large" color="black" />
) : (
<>
<FolloweringScreens data={followingData} />
</>
)}
</>
);
}
export default Following;
I keep getting this error: TypeError: Cannot read property 'data' of undefined, when there is no data being passed to my shopping cart page. How can I fix this error? Ideally, I would just like the page to display: "This cart is empty". I tried adding a conditional statement above the UserCardBlock, but it did not change anything. Thank you
import React, { useState } from 'react'
import { useDispatch } from 'react-redux';
import {
removeCartItem,
onSuccessBuy
} from '../../../_actions/user_actions';
import UserCardBlock from './Sections/UserCardBlock';
import { Result, Empty, Button } from 'antd';
import Paypal from '../../utils/Paypal';
function CartPage(props) {
const dispatch = useDispatch();
console.log(props)
const [Total, setTotal] = useState(props.location.state.data.price)
const [ShowTotal, setShowTotal] = useState(true)
const [ShowSuccess, setShowSuccess] = useState(false)
const removeFromCart = (productId) => {
dispatch(removeCartItem(productId))
}
const transactionSuccess = (data) => {
dispatch(onSuccessBuy({
cartDetail: props.user.cartDetail,
paymentData: data
}))
.then(response => {
setShowSuccess(true)
setShowTotal(false)
}
)
}
const transactionError = () => {
console.log('Paypal error')
}
const transactionCanceled = () => {
console.log('Transaction canceled')
}
const propductList = (data) =>{
console.log(data)
setTotal(data)
}
return (
<div style={{ width: '85%', margin: '3rem auto' }}>
<h1>My Cart</h1>
<div>
<UserCardBlock
productData={props.location.state.data}
removeItem={removeFromCart}
productList={data => propductList(data)}
/>
{ShowTotal ? (
<div style={{ marginTop: "3rem" }}>
<h2>Total amount: ${Total * 15} </h2>
</div>
) : ShowSuccess ? (
<Result status="success" title="Successfully Purchased Items" />
) : (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<br />
<Empty description={false} />
<p>No Items In The Cart</p>
</div>
)}
</div>
{/* Paypal Button */}
{ShowTotal &&
<Paypal
toPay={Total}
onSuccess={transactionSuccess}
transactionError={transactionError}
transactionCanceled={transactionCanceled}
/>
}
</div>
)
}
export default CartPage
Seems like your component is dependent on location state.
const [Total, setTotal] = useState(props.location.state.data.price)
and
<UserCardBlock
productData={props.location.state.data}
Try using optional chaining and nullish coalescing
const [Total, setTotal] = useState(props.location?.state?.data?.price ?? 0)
<UserCardBlock
productData={props.location.state?.data ?? []}
It seems like you are using redux so i will suggest you to use redux store instead of location state.
I'm not 100% sure what's going on here. I've got a display component that displays a bunch of cards, using a map based on my database - On the card is an edit button that pops a modal up, passing props over to the edit form.. Here's kinda how it looks:
import React, { useState } from 'react'
import { useQuery, useMutation } from '#apollo/client'
import { GET_ALL_PROJECTS, REMOVE_PROJECT } from '../helpers/queries'
import { makeStyles } from '#material-ui/core/styles'
import DeleteIcon from '#material-ui/icons/Delete'
import EditIcon from '#material-ui/icons/Edit'
import AddForm from './AddForm'
import EditForm from './EditForm'
import AlertMessage from '../Alerts/AlertMessage'
import { Grid, Typography, Card, CardActionArea, CardActions, CardContent, CardMedia, Button, Modal, Backdrop, Fade } from '#material-ui/core'
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
const DisplayProjects = () => {
const styles = useStyles()
const [deleteItem] = useMutation(REMOVE_PROJECT)
const { loading, error, data } = useQuery(GET_ALL_PROJECTS)
const [status, setStatusBase] = useState('')
const [resultMessage, setResultMessage] = useState('')
const [addOpen, setAddOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
const onDelete = (id, e) => {
e.preventDefault()
deleteItem({
variables: { id },
refetchQueries: [{ query: GET_ALL_PROJECTS }]
}).then(
res => handleSuccess(res),
err => handleError(err)
)
}
// Handles Result of the Delete Operation
const handleSuccess = (res) => {
console.log(res.data.deleteProject.proj_name)
// console.log('success!');
setResultMessage(res.data.deleteProject.proj_name)
setStatusBase({
msg: `Successfully Deleted ${resultMessage}`,
key: Math.random()
})
}
const handleError = (err) => {
console.log('error')
}
//Handles the Modal for Add Project
const handleAddOpen = () => {
setAddOpen(true);
};
const handleAddClose = () => {
setAddOpen(false);
};
//Handles the Modal for Edit Project
const handleEditOpen = () => {
setEditOpen(true);
};
const handleEditClose = () => {
setEditOpen(false);
};
if (loading) return '...Loading'
if (error) return `Error: ${error.message}`
return (
<div>
<div style={{ marginTop: 20, padding: 30 }}>
<Grid container spacing={8} justify='center' alignItems='center'>
{data.projects.map(p => {
return (
<Grid item key={p._id}>
<Card >
<CardActionArea>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<CardMedia
style={{ width: 400, height: 100, paddingTop: 10, }}
component='img'
alt='Project Image'
height='140'
image={require('../../images/html-css-javascript-lg.jpg')}
/>
</div>
<CardContent >
<Typography gutterBottom variant='h5' component="h2">
{p.proj_name}
</Typography>
<Typography component='p'>
{p.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button>
<DeleteIcon onClick={e => onDelete(p._id, e)} />
</Button>
<Button onClick={handleEditOpen}>
<Modal
open={editOpen}
onClose={handleEditClose}
closeAfterTransition
BackdropComponent={Backdrop}
className={styles.modal}
>
<Fade in={editOpen}>
<div className={styles.paper}>
<EditForm
id={p._id}
close={handleEditClose}
name={p.proj_name}
desc={p.description}
gh={p.gh_link}
live={p.live_link}
img={p.image_url}
/>
</div>
</Fade>
</Modal>
<EditIcon />
</Button>
</CardActions>
</Card>
{ status ? <AlertMessage key={status.key} message={status.msg} /> : null}
</Grid>
)
}
)}
</Grid>
<Button type='button' onClick={handleAddOpen}>Add Project</Button>
<Modal
open={addOpen}
onClose={handleAddClose}
closeAfterTransition
BackdropComponent={Backdrop}
className={styles.modal}
>
<Fade in={addOpen}>
<div className={styles.paper}>
<AddForm close={handleAddClose} />
</div>
</Fade>
</Modal>
</div>
</div >
)
}
export default DisplayProjects
And here's the form. I've destructured out the props into variables and placed them into a state object called details, so they can be overwritten and submitted to the database..
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import { useMutation, useQuery } from '#apollo/client'
import { EDIT_PROJECT, GET_ALL_PROJECTS, GET_PROJECT_BY_ID} from '../helpers/queries'
const AddForm = (props) => {
const params = useParams()
const id = params.toString()
// console.log(id);
const [editProjectItem] = useMutation(EDIT_PROJECT)
const {loading, data, error} = useQuery(GET_PROJECT_BY_ID, {
variables: {
id
},
})
const [details, setDetails] = useState({})
if (loading) return '...Loading';
if (error) return <p>ERROR: {error.message}</p>;
if (!data) return <p>Not found</p>;
setDetails(data.projectById)
console.log(data.projectById)
const submitForm = e => {
e.preventDefault()
try {
editProjectItem({
variables: { id, proj_name, description, gh_link, live_link, image_url},
refetchQueries: [{query: GET_ALL_PROJECTS}]
})
}
catch (err) {
console.log('You Goofed')
}
// setDetails({
// proj_name: '',
// description: '',
// gh_link: '',
// live_link: '',
// image_url: ''
// })
props.close()
}
const changeDetails = (e) => {
setDetails({
...details,
[e.target.name]: e.target.value
})
}
const {_id, proj_name, description, gh_link, live_link, image_url} = details
return (
<div key = {_id}>
<h2>Edit {proj_name}</h2>
<form onSubmit = {submitForm} >
<label>
Project Name:
<input
name = 'proj_name'
value = {proj_name}
onChange = {changeDetails}
/>
</label>
<label>Description</label>
<input
name = 'description'
value = {description}
onChange = {changeDetails}
/>
<label>GitHub Link</label>
<input
name = 'gh_link'
value = {gh_link}
onChange = {changeDetails}
/>
<label>Live Link</label>
<input
name = 'live_link'
value = {live_link}
onChange = {changeDetails}
/>
<label>Preview Image</label>
<input
name = 'image_url'
value = {image_url}
onChange = {changeDetails}
/>
<button type = 'submit'>Submit</button>
</form>
</div>
)
}
export default AddForm
The problem I'm running into, is that when I access the modal, the props are sent from literally EVERY Object, instead of the one, and displays the data for the last record instead of the one I want to edit You can see what happens here (I logged props.id in order to test) https://imgur.com/a/pcEKl89
What did I miss? (Disclaimer: I am still a student, and learning the craft.. be gentle on my code please)
EDIT: I just realized that I didn't indicate that this is the final form of the EditForm component. I haven't added the logic in to make the updates yet, I just wanted to get the data showing properly first.
EDIT2: I made some changes to how the ID is passed over, I was already using React-Router, so I went ahead and made a route to /edit/:id and then using useParams(), I got the ID that way. It seems to be working, however now I'm getting a Too many re-renders message. Updated the AddForm code above to reflect the changes..
I figured out the re-render issue.. it was as simple as dropping the setDetails function into a useEffect Hook:
useEffect(()=> {
if(data){
setDetails(data.projectById)
}
},[data])