Reactでプレビュー機能付きの画像アップローダー(ドラッグもあり)
個人開発のLaravelサイトをReact化してみたのだけども、
よくあるプレビュー機能付きの画像アップローダー
みたいなのが以外と面倒だったのでちょいと書いてみます。
サンプル
宣伝がてらですが、参考ページは下記です。
https://wordstacks.nocebo.jp/posts/create
の“ここにファイルをドロップ”の部分です。
べっ、別に投稿してもいいんだからね!
とりあえず抜粋だけど全文
全部入ってるとわかりにくい気がしたので
色々省略してますがこんな感じになりました。
雰囲気Reactと雰囲気Typescriptなので
細かい事はご愛嬌。
ちょっとした参考になれば幸いです。
型付けとか特に適当
anyだっていいじゃないか、動くんだもの
anyだっていいじゃないか、動くんだもの #エンジニア #javascript #typescript #プログラム https://t.co/cwhV9YAf2c
— wordstacks (@wordstacks_bot) February 5, 2020
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { PostActions } from "@/module/post";
import styled from "styled-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// ---------------------------
const Root = styled.div`
/* スタイル省略 */
`;
const Preview = styled.div`
/* スタイル省略 */
`;
const UI = styled.div`
/* スタイル省略 */
.area[data-drag="true"] {
/* ドラッグ時のスタイル */
}
`;
// ---------------------------
const Images: React.FC = props => {
const dispatch = useDispatch();
const [isDrag, setIsDrag] = useState(false);
const [images, setImages] = useState([]);
useEffect(() => {
dispatch(PostActions.updateImages(images);
}, [images]);
// ファイル選択&ドラッグからの処理
const handleChangeFile = files => {
const merge: Array<Object> = [];
(async () => {
await Promise.all(
Object.keys(files).map(async item => {
const file: File = files[Number(item)];
const reader: FileReader = await readerOnloadend(file);
merge.push({ file: file, previewUrl: reader.result });
})
);
setImages([...images, ...merge]);
})();
};
// await reader.onloadend
const readerOnloadend = async (data): Promise<FileReader> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader);
};
reader.readAsDataURL(data);
});
};
// 削除
const remove = (index: number) => {
images.splice(index, 1);
setImages([...images]);
};
// クリア
const clear = () => {
setImages([]);
};
// ドラッグ状態
const dragState = (e, state) => {
e.preventDefault();
setIsDrag(state);
};
// プレビュー画像表示
const previews = Array.from(images || []).map(
(image: any, index: number) => {
return (
<div className="element" key={index}>
<img src={image.previewUrl} alt="プレビュー画像" />
<button
onClick={(e: any) => {
remove(index);
}}
>
<FontAwesomeIcon icon={["fas", "minus"]} />
</button>
</div>
);
}
);
// EDIT時にすでに登録済みのstrageからの画像をfile(blob)に変換
const { records, isRecord } = useSelector((state: any) => state.posts);
useEffect(() => {
if (!isRecord || !records[0]) return;
dispatch(PostActions.isRecord(false));
(async () => {
const recordImages: Array<Object> = [];
await Promise.all(
records.map(async item => {
const response = await fetch(`/storage/${item.path}`);
const arrayBuffer = await response.arrayBuffer();
const blob = new Blob([arrayBuffer], {type: "image/jpeg"});
const reader: FileReader = await readerOnloadend(blob);
recordImages.push({
file: new File([blob], "name"),
previewUrl: reader.result
});
})
);
setImages([...recordImages]);
})();
}, [isRecord]);
// ---------------------------
return (
<Root>
<Preview>{previews}</Preview>
<UI>
<div
className="area"
data-drag={isDrag}
onDragEnter={e => {dragState(e, true)}}
onDragLeave={e => {dragState(e, false)}}
onDragOver={e => {dragState(e, true)}}
onDrop={e => {
dragState(e, false);
handleChangeFile(e.dataTransfer.files);
}}
>
<p>ここにファイルをドロップ</p>
<span>または</span>
<label>
ファイルを選択
<input
type="file"
accept="image/*"
multiple
onChange={e => handleChangeFile(e.target.files)}
/>
</label>
</div>
<div className="clear">
<button onClick={clear}>クリア</button>
</div>
</UI>
</Root>
);
};
export default Images;
ハイライト表示効いてない?かも。
あとはちょっとした補足等
handleChangeFile
useEffect(() => {
dispatch(PostActions.updateImages(images);
}, [images]);
const handleChangeFile = files => {
const merge: Array<Object> = [];
(async () => {
await Promise.all(
Object.keys(files).map(async item => {
const file: File = files[Number(item)];
const reader: FileReader = await readerOnloadend(file);
merge.push({ file: file, previewUrl: reader.result });
})
);
setImages([...images, ...merge]);
})();
};
ドラッグとファイルを選択。で使ってるハンドラです。
特にはないけどawaitで読めたらuseEffectでreduxに反映してます
readerOnloadend
const readerOnloadend = async (data): Promise<FileReader> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader);
};
reader.readAsDataURL(data);
});
};
await reader.onloadend()
って書きたくなるけどそのままだと出来ないっぽいので
上記な感じにしてみた。
readAsDataURL
readAsDataURLはBlobかFileオブジェクトを読めるっぽい。
今回ので言うと
handleChangeFileの時はFileで
editの時useEffectでstorageの画像ひっぱってくる時はBlobです。
お?こういう時にジェネリクス使えばいいのかな?
って思ってちょっと型付け試してみたけど、
async使ってどう書けばよいのか、サクッとわからないので諦めた。
creteとeditで使いまわしたい
のでeditの場合に下記のような感じにしました。
const { records, isRecord } = useSelector((state: any) => state.posts);
useEffect(() => {
if (!isRecord || !records[0]) return;
dispatch(PostActions.isRecord(false));
(async () => {
const recordImages: Array<Object> = [];
await Promise.all(
records.map(async item => {
const response = await fetch(`/storage/${item.path}`);
const arrayBuffer = await response.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: "image/jpeg" });
const reader: FileReader = await readerOnloadend(blob);
recordImages.push({
file: new File([blob], "name"),
previewUrl: reader.result
});
})
);
setImages([...recordImages]);
})();
}, [isRecord]);
ここが個人的に結構めんどくさいポイントですが
普通にリモートにある画像imgのまま読むだけだと
Laravel側に送った時にupdateが出来ないっぽいのと
ハンドラから反映するimagesの型と合わせないと
下のpreviewsで使いまわせないのでBlob読んでFileにしました。
Blobはバイナリ、らしい。
const previews = Array.from(images || []).map((image: any, index: number) => {
return (
<div className="element" key={index}>
<img src={image.previewUrl} alt="プレビュー画像" />
<button
onClick={(e: any) => {
remove(index);
}}
>
<FontAwesomeIcon icon={["fas", "minus"]} />
</button>
</div>
);
});
以上です。
ディスカッション
コメント一覧
まだ、コメントがありません