Maybe that's a stupid question, but I have a problem. My state looks like this:
const initialState: PhotoState = {
photos: [],
};
The reducer code looks like this:
const initialState: PhotoState = {
photos: [],
};
export default function photoReducer(state = initialState, action: PhotoActionTypes): PhotoState {
switch (action.type) {
case SET_PHOTOS:
const photos: any[] = action.payload;
return {...state, photos};
}
return state;
};
I get photos from API and then set them this way:
export function setPhotos(payload: any[]): PhotoActionTypes {
return {type: SET_PHOTOS, payload};
}
export function getPhotos() {
return (dispatch: Dispatch<PhotoActionTypes>, getState: () => RootState): void => {
const profile_id = getState().auth.profile_id;
ax().post('pictures/api/pictures/list', {profile_id}).then((response) => {
const photos: any[] = response.data.pictures || [];
dispatch(setPhotos(photos));
})
}
}
Also I have an action that sends a new photo to the server and saves it in history. Then I get photos in component:
useEffect(() => {
dispatch(getPhotos());
}, []);
const handleSendPhoto = (): void => {
dispatch(sendPhoto(image?.base64));
dispatch(getPhotos());
}
The whole component:
const PhotoScreen = () => {
const [photoFlag, setPhotoFlag] = useState(false);
const [image, setImage] = useState<TakePictureResponse | null>(null);
const [height, setHeight] = useState(0);
const width = Dimensions.get('screen').width / 5;
const dispatch = useDispatch();
const handleSendPhoto = (): void => {
dispatch(sendPhoto(image?.base64, location));
dispatch(getPhotos());
}
const PhotoView = () => (
<View>
<FastImage
style={{width: width, height: height}}
source={{
uri: `data:image/jpeg;base64, ${image?.base64}`,
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.contain}
onLoad={evt => {
setHeight(evt.nativeEvent.height / evt.nativeEvent.width * width)
}}
/>
<Button
mode="contained"
onPress={handleSendPhoto}
disabled={!image}
color={constants.buttonColor}>
Add photo
</Button>
</View>
);
return (
<SafeAreaView style={{...mainStyles.screen, ...styles.container}}>
<StatusBarDark />
{!photoFlag && (<View>
<Button
mode="contained"
onPress={() => setPhotoFlag(true)}
color={constants.buttonColor}>
Make photo
</Button>
</View>)}
{photoFlag && <CameraComponent setImage={setImage} setPhotoFlag={setPhotoFlag}/>}
{image !== null && <PhotoView />}
</SafeAreaView>
);
};
export default PhotoScreen;
But state updates only from the second time. I press the button 'Add photo', photo appends to history, but doesn't shows up in it. Then I press the button again, and previous photo shows up in history, but current photo doesn't.
How can I fix it?
UPD: Problem was solved. The question may be closed.
You shouldn't do this, because both calls will be send at the same time:
const handleSendPhoto = (): void => {
dispatch(sendPhoto(image?.base64));
dispatch(getPhotos()); // this will be called before the upload is finished. so the old data will be returned.
}
you may need to use redux-thrunk (https://github.com/reduxjs/redux-thunk) like this:
const handleSendPhoto = (): void => {
dispatch(sendPhoto(image?.base64)).then(() => dispatch(getPhotos()));
}
Related
I'm trying to delete item in redux toolkit, but don't know how, the remove function only work on screen, i have to press twice to delete the previous one
Here is the reducer
const noteReducer = createSlice({
name: "note",
initialState: NoteList,
reducers: {
addNote: (state, action: PayloadAction<NoteI>) => {
const newNote: NoteI = {
id: new Date(),
header: action.payload.header,
note: action.payload.note,
date: new Date(),
selectStatus: false,
};
state.push(newNote);
},
removeNote: (state, action: PayloadAction<NoteI>) => { //
======> Problem here
return state.filter((item) => item.id !== action.payload.id);
},
toggleSelect: (state, action: PayloadAction<NoteI>) => {
return state.map((item) => {
if (item.id === action.payload.id) {
return { ...item, selectStatus: !item.selectStatus };
}
return item;
});
},
loadDefault: (state) => {
return state.map((item) => {
return { ...item, selectStatus: false };
});
},
resetNote: (state) => {
return (state = []);
},
editNote: (state, action: PayloadAction<NoteI>) => {
return state.map((item) => {
if (item.id === action.payload.id) {
return {
...item,
note: action.payload.note,
header: action.payload.header,
date: action.payload.date,
};
}
return item;
});
},
},
extraReducers: (builder) => {
builder.addCase(fetchNote.fulfilled, (state, action) => {
state = [];
return state.concat(action.payload);
});
},
});
Here is the function where i use it:
CODE UPDATED
export default function NoteList(props: noteListI) {
const { title, note, id, date } = props;
const data = useSelector((state: RootState) => state.persistedReducer.note);
useEffect(() => {
currentDate.current = data;
}, [data]);
const removeSelectedNote = () => {
dispatch(removeNote({ id: id }));
console.log(data); ====> still log 4 if i have 4
};
console.log(data); // ====> work if i log here but a lots of logs
return (
<View>
<TouchableOpacity
onLongPress={() => {
removeSelectedNote();
console.log("current", currentDate.current); ///same
}}
// flex
style={CONTAINER}
onPress={() =>
!toggleSelectedButton ? onNavDetail() : setEnableToggle()
}
>
<Note
note={note}
header={title}
date={date}
id={id}
selectedStatus={selectedButtonStatus}
/>
</TouchableOpacity>
</View>
);
}
I have to press twice to make it work, for example, i have 4 item, when i press one, the item on screen disappears but the data log still have 4 item, when i click another, it show 3 on console.log but the screen display 2, the redux state is change outside the return() but i can't capture the updated state, it work the previous one
Here is a gif to show what going on
When i press only one item, it change on UI but when i refresh it return same state
When i click twice or more, it make changes to previous
Updated
The redux-persist code:
const reducer = combineReducers({
note: noteReducer,
firebase: authentication,
});
const persistConfig = {
key: "root",
storage: AsyncStorage,
blacklist: [],
};
const persistedReducer = persistReducer(persistConfig, reducer);
const store = configureStore({
reducer: { persistedReducer, toggle: toggleReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const persistStorageNote = persistStore(store);
I also added the useEffect by this, but problem is when i log the changes in function, it remain the same:
here is how you can log updated data correctly, as state update is asynchronous it doesn’t change immediately when you dispatch removeNote
export default function NoteList(props: noteListI) {
const { title, note, id, date } = props;
const data = useSelector((state: RootState) => state.persistedReducer.note);
// log changed data
useEffect(() => {
console.log(data);
}, [data]);
const removeSelectedNote = () => {
dispatch(removeNote({ id: id }));
};
return (
<View>
<TouchableOpacity
onLongPress={() => {
removeSelectedNote();
}}
// flex
style={CONTAINER}
onPress={() =>
!toggleSelectedButton ? onNavDetail() : setEnableToggle()
}
>
<Note
note={note}
header={title}
date={date}
id={id}
selectedStatus={selectedButtonStatus}
/>
</TouchableOpacity>
</View>
);
}
about reloading issue, try to close the app and open it like a user of your app would (minimize the app -> remove the app from recently opened apps -> open app again ) , instead of reloading the project.
I'm learning redux and i'm making a sort of pokedex app where i fetch 20 pokemons from pokeapi.co at a time. When the page changes a new list of 20 pokemons is fetched. The problem is that while state changes to the new pokemons, they don't actually render.
App.js
const App = () => {
const dispatch = useDispatch();
const offset = useSelector(state => state.offset);
const limit = useSelector(state => state.limit);
useEffect(() => {
//FETCHES 20 OBJECTS THAT CONTAIN AN URL TO AN INDIVIDUAL POKEMON
dispatch(fetchPokemons(limit, offset));
}, [limit, offset, dispatch]);
...
return (
<div style={{ backgroundColor: '#222222' }}>
<Notification />
<AppBarPokemon />
<Switch>
<Route path="/pokemons">
<PokemonsDisplay CapsFirstLetter={CapsFirstLetter}/>
</Route>
...
PokemonsDisplay.js
const PokemonsDisplay = ({ CapsFirstLetter }) => {
const dispatch = useDispatch();
const classes = useStyles();
const pokemons = useSelector(state => state.pokemons);
console.log(pokemons);
const pageSize = 20;
const totalCount = 898;
const handleClick = (p) => {
dispatch(getOnePokemon(p));
};
return (
<div className={classes.root}>
{pokemons && (
<Grid container spacing={3}>
{pokemons.map(p => (
<Grid item xs={3} key={p.name} className={classes.gridItem} component={Link} onClick={() => handleClick(p)} to={`/pokemons/${p.name}`} data-cy={`pokemon-button-${p.name}`}>
<Paper className={classes.paper && classes.color} elevation={10}>
<p className={classes.p}>#{p.id}</p>
<p className={classes.p}>{CapsFirstLetter(p.name)}</p>
{p.sprites.other["dream_world"]["front_default"] !== null ?
<img className={classes.image} alt={`${p.name}'s sprite`} src={p.sprites.other["dream_world"]["front_default"]}/> :
<img className={classes.image} alt={`${p.name}'s sprite`} src={p.sprites.other["official-artwork"]["front_default"]}/>}
</Paper>
</Grid>
))}
</Grid>
)}
<Pagination
totalCount={totalCount}
pageSize={pageSize}
/>
</div>
);
};
pokemonsReducer.js
import getPokemons from '../services/pokemons';
import axios from 'axios';
import { loadPokemonsFromLS, savePokemonsList } from '../utils/localStoragePokemons';
const pokemonsReducer = (state = [], action) => {
console.log('state is:', state)
switch(action.type){
case 'INIT_POKEMONS':
return action.data;
default:
return state;
};
};
export const fetchPokemons = (limit, offset) => {
return async dispatch => {
try {
const pokemons = loadPokemonsFromLS(limit, offset);
dispatch({
type: 'INIT_POKEMONS',
data: pokemons
});
} catch (error) {
const pokemons = await getPokemons.getPokemons(limit, offset);
let pokemonsArray = [];
let pokemonsObject = {};
pokemons.results.forEach(async (r, i) => {
//FETCHES EACH POKEMON URL AND STORES ITS DATA ON pokemons STATE
const pokemonNow = await axios.get(r.url);
pokemonsArray.push(pokemonNow.data);
pokemonsObject[i] = pokemonNow.data
});
savePokemonsList(limit, offset, pokemonsObject);
dispatch({
type: 'INIT_POKEMONS',
data: pokemonsArray
});
}
};
};
export default pokemonsReducer;
I have tried to dispatch({ data: [...pokemons] })
But it doesnt work.
Also i forgot to add. When i go to my component that is routed to '/' and then back to '/pokemons' they render.
Edit: I think i'm getting there.
i changed the reducer function so that it gets called independently from the dispatch, the problem is that now the action doesn't get fired xD.
export const fetchEverything = async (limit, offset) => {
try {
const pokemons = loadPokemonsFromLS(limit, offset);
initPokemons(pokemons);
} catch (error) {
const pokemonsData = await getPokemons.getPokemons(limit, offset);
let pokemons = [];
let pokemonsObject = {};
console.log(pokemonsData)
pokemonsData.results.forEach(async (r, i) => {
//FETCHES EACH POKEMON URL AND STORES ITS DATA ON pokemons STATE
const pokemonNow = await axios.get(r.url);
pokemonsObject[i] = pokemonNow.data;
//console.log([pokemonNow.data][0]);
pokemons.push(pokemonNow.data);
});
console.log(pokemons);
console.log(pokemonsObject);
savePokemonsList(limit, offset, pokemonsObject);
initPokemons(pokemons);
};
};
export const initPokemons = (pokemons) => {
return dispatch => dispatch({ type: 'INIT_POKEMONS', pokemons: pokemons })
};
const pokemonsReducer = (state = [], action) => {
switch(action.type){
case 'INIT_POKEMONS':
console.log(action);
const newState = action.pokemons
return newState;
default:
return state;
};
};
It happens due to redux state mutation, you can resolve this issue using immer as stated in redux documentation as well. https://www.npmjs.com/package/immer
I have this context:
interface AlertContextProps {
show: (message: string, duration: number) => void;
}
export const AlertContext = createContext<AlertContextProps>({
show: (message: string, duration: number) => {
return;
},
});
export const AlertProvider: FC<IProps> = ({ children }: IProps) => {
const [alerts, setAlerts] = useState<JSX.Element[]>([]);
const show = (message: string, duration = 6000) => {
let alertKey = Math.random() * 100000;
setAlerts([...alerts, <Alert message={message} duration={duration} color={''} key={alertKey} />]);
setTimeout(() => {
setAlerts(alerts.filter((i) => i.key !== alertKey));
}, duration + 2000);
};
return (
<>
{alerts}
<AlertContext.Provider value={{ show }}>{children}</AlertContext.Provider>
</>
);
};
which I need to "translate" into a redux slice. I got a hang of everything, apart from the show method. What would be the correct way to treat it? I was thinking about a thunk, but it's not really a thunk. Making it a reducer with setTimeout also seems like an ugly thing to do. So how would you guys do it?
My code so far:
type Alert = [];
const initialState: Alert = [];
export const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
setAlertState(state, { payload }: PayloadAction<Alert>) {
return payload;
},
},
});
export const { setAlertState } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;
The timeout is a side effect so you could implement that in a thunk.
You have an action that shows an alert message that has a payload of message, id and time to display, when that time runs out then the alert message needs to be removed so you need a remove alert message action as well that is dispatched from the thunk with a payload of the id of the alert message.
I am not sure why you add 2 seconds to the time to hide the message duration + 2000 since the caller can decide how long the message should show I don't think it should half ignore that value and randomly add 2 seconds.
Here is a redux example of the alert message:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
messages: [],
};
//action types
const ADD_MESSAGE = 'ADD_MESSAGE';
const REMOVE_MESSAGE = 'REMOVE_MESSAGE';
//action creators
const addMessage = (id, text, time = 2000) => ({
type: ADD_MESSAGE,
payload: { id, text, time },
});
const removeMessage = (id) => ({
type: REMOVE_MESSAGE,
payload: id,
});
//id generating function
const getId = (
(id) => () =>
id++
)(1);
const addMessageThunk = (message, time) => (dispatch) => {
const id = getId();
dispatch(addMessage(id, message, time));
setTimeout(() => dispatch(removeMessage(id)), time);
};
const reducer = (state, { type, payload }) => {
if (type === ADD_MESSAGE) {
return {
...state,
messages: state.messages.concat(payload),
};
}
if (type === REMOVE_MESSAGE) {
return {
...state,
messages: state.messages.filter(
({ id }) => id !== payload
),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//simple implementation of thunk (not official redux-thunk)
({ dispatch }) =>
(next) =>
(action) =>
typeof action === 'function'
? action(dispatch)
: next(action)
)
)
);
const App = () => {
const messages = useSelector(selectMessages);
const dispatch = useDispatch();
return (
<div>
<button
onClick={() =>
dispatch(addMessageThunk('hello world', 1000))
}
>
Add message
</button>
<ul>
{messages.map((message) => (
<li key={message.id}>{message.text}</li>
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
#HMR's use of a thunk is fine, but I don't like what they've done to your reducer. You're already using redux-toolkit which is great! redux-toolkit actually includes and exports a nanoid function which they use behind the scenes to create unique ids for thunks. You can use that instead of Math.random() * 100000.
I always start by thinking about types. What is an Alert? You don't want to store the <Alert/> because a JSX.Element is not serializable. Instead you should just store the props. You'll definitely store the message and key/id. If you handle expiration on the front-end then you would also store the duration, but if the expiration is handled by a thunk then I don't think you need it in the redux state or component props.
It seems like you want to allow multiple alerts at one time, so return payload is not going to cut it for your reducer. You'll need to store an array or a keyed object will all of your active alerts.
You absolute should not use setTimeout in a reducer because that is a side effect. You can use it either in a thunk or in a useEffect in the Alert component. My inclination is towards the component because it seems like the alert should probably be dismissible as well? So you can use the same function for handling dismiss clicks and automated timeouts.
We can define the info that we want to store for each alert.
type AlertData = {
message: string;
id: string;
duration: number;
}
And the info that we need to create that alert, which is the same but without the id because we will generate the id in the reducer.
type AlertPayload = Omit<AlertData, 'id'>
Our state can be an array of alerts:
const initialState: AlertData[] = [];
We need actions to add a new alert and to remove an alert once it has expired.
import { createSlice, PayloadAction, nanoid } from "#reduxjs/toolkit";
...
export const alertSlice = createSlice({
name: "alert",
initialState,
reducers: {
addAlert: (state, { payload }: PayloadAction<AlertPayload>) => {
const id = nanoid(); // create unique id
state.push({ ...payload, id }); // add to the state
},
removeAlert: (state, { payload }: PayloadAction<string>) => {
// filter the array -- payload is the id
return state.filter((alert) => alert.id !== payload);
}
}
});
export const { addAlert, removeAlert } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;
So now to the components. What I have in mind is that you would use a selector to select all of the alerts and then each alert will be responsible for its own expiration.
export const AlertComponent = ({ message, duration, id }: AlertData) => {
const dispatch = useDispatch();
// function called when dismissed, either by click or by timeout
// useCallback is just so this can be a useEffect dependency and won't get recreated
const remove = useCallback(() => {
dispatch(removeAlert(id));
}, [dispatch, id]);
// automatically expire after the duration, or if this component unmounts
useEffect(() => {
setTimeout(remove, duration);
return remove;
}, [remove, duration]);
return (
<Alert
onClose={remove} // can call remove directly by clicking the X
dismissible
>
<Alert.Heading>Alert!</Alert.Heading>
<p>{message}</p>
</Alert>
);
};
export const ActiveAlerts = () => {
const alerts = useSelector((state) => state.alerts);
return (
<>
{alerts.map((props) => (
<AlertComponent {...props} key={props.id} />
))}
</>
);
};
I also made a component to create alerts to test this out and make sure that it works!
export const AlertCreator = () => {
const dispatch = useDispatch();
const [message, setMessage] = useState("");
const [duration, setDuration] = useState(8000);
return (
<div>
<h1>Create Alert</h1>
<label>
Message
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</label>
<label>
Duration
<input
type="number"
step="1000"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value, 10))}
/>
</label>
<button
onClick={() => {
dispatch(addAlert({ message, duration }));
setMessage("");
}}
>
Create
</button>
</div>
);
};
const App = () => (
<div>
<AlertCreator />
<ActiveAlerts />
</div>
);
export default App;
Code Sandbox Link
I'm trying to make todo app but,
Every time I input something, my redux developer tools shows an '' (empty string)
my reduce look like this
const TodoReducer = (state = initialState, action) => {
switch(action.type){
case 'ADD_TODO':
return {
...state,
todoList: [...state.todoList, action.item]
}
}
}
export default TodoReducer;
my action
export const addTodo = () => {
return {
type: 'ADD_TODO',
item: ''
}
}
and the App.js
function App() {
const [input, setInput] = useState('');
const todoList = useSelector(state => state.todoList)
const dispatch = useDispatch();
const addHandler = () => {
console.log(`adding ${input}`)
dispatch(addTodo({
item: input
}))
setInput('');
}
return (
<div>
<p>TODO</p>
<p>{todoList}</p>
<input type="text"
value={input}
onChange={e=> setInput(e.target.value)}
/>
<button type="button" onClick={addHandler}>Add</button>
</div>
);
}
thank you in advance. any help is appreciated
You forgot to pass the item as an input to the action and return
Change:
export const addTodo = () => {
return {
type: 'ADD_TODO',
item: ''
}
}
To:
export const addTodo = (item) => {
return {
type: 'ADD_TODO',
item: item
}
}
And
Change
<p>{todoList}</p>
To
<p>
{Array.isArray(todoList) &&
todoList.map((item, itemIndex) => (
<div key={itemIndex}>{item}</div>
))}
</p>
When I dispatch my delete action to Redux I am getting the error unidentified is not an object evaluating selectedUser.imageUri Everything is being loaded from a server and I know the delete action works as it deletes the object from the server however I get this error and the screen only updates when I reload the application. Please can someone help me I really need your help. Thank you so much in advance!!!I am even checking to see if there is no object in the selecetedUser array then render an image called nothing.png
This is my code where I am seeing the error
const Viewer = (props) => {
const userID = props.navigation.getParam('id')
//Nothing is just a picture when there are no images
import nothing from './Images/nothing.png'
const selectedUser = useSelector(state => state.user.user.find(user => user.id === userID))
const cBimageUri = {uri: selectedUser.imageUri }
const checkImage = cBimageUri.length === 0? nothing : cBimageUri
const cBimageUri = {uri: selectedUser.imageUri }
const deleteCb = useCallback(() =>{
dispatch(deleteUser(userID))
props.navigation.goBack()
},[userID])
useEffect(() => {
props.navigation.setParams({deleteCb: deleteCb})
},[deleteCb])
return (
<ScrollView style={{backgroundColor: 'white'}}>
<Image source={checkImage} style={styles.image}/>
<Text style={styles.name}>{selectedCookBook.name}</Text>
</ScrollView>
)
}
export default Viewer
Redux reducer
import { CREATE_USER, DELETE_USER } from '../actions/account'
const initialState = {
account: [],
}
const USerReducer = (state=initialState, action) =>{
switch(action.type){
case CREATE_USER:
const newUser = new MyUser(
action.userData.id,
action.userData.name,
action.userData.image,
)
return { ...state, user: state.account.concat(newUser)}
case DELETE_USER:
const filteredItems = state.account.filter(cb => cb.id !== action.deleteCb)
return {account: filteredItems }
default:
return state
}
}
export default USerReducer
Redux action
export const DELETE_COOKBOOK = 'CLEAR'
export const deleteCookbook = (deleteCb) => {
return {type: DELETE_COOKBOOK, deleteCb: deleteCb}
}
console logging selectedUser
[
Object {
"id": 1595444079901,
"val": "Veveve",
},
name: John Snow,
imageUri: 'file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540anonymous%252Frn-first-app-e648c632-2715-4169-abf3-e0cdbe2ac7d5/ImagePicker/461b63af-a908-47e9-8841-d5d8f2c4eb67.jpg
file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540anonymous%252Frn-first-app-e648c632-2715-4169-abf3-e0cdbe2ac7d5/ImagePicker/461b63af-a908-47e9-8841-d5d8f2c4eb67.jpg'
}
]
Try to change follow
const Viewer = (props) => {
const userID = props.navigation.getParam('id')
//Nothing is just a picture when there are no images
import nothing from './Images/nothing.png'
const selectedUser = useSelector(state => state.user.user.find(user => user.id === userID))
const cBimageUri = selectedUser.imageUri // --> changed it
const checkImage = cBimageUri.length === 0? nothing : cBimageUri
//const cBimageUri = {uri: selectedUser.imageUri } // is it a right row? just repeat previous
const deleteCb = useCallback(() =>{
dispatch(deleteUser(userID))
props.navigation.goBack()
},[userID])
useEffect(() => {
props.navigation.setParams({deleteCb: deleteCb})
},[deleteCb])
return (
<ScrollView style={{backgroundColor: 'white'}}>
<Image source={{uri: checkImage}} style={styles.image}/> // --> changed it. Not sure about it, if it's not working check below link
<Text style={styles.name}>{selectedCookBook.name}</Text>
</ScrollView>
)
}
export default Viewer
this link https://stackoverflow.com/questions/50249353/uri-vs-url-in-react-native